fix(heartbeat): observe the installed automation prompt binding every turn - #4425
huangruiteng wants to merge 1 commit into
Conversation
… 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>
8756a6b to
fc8e373
Compare
|
Superseded by #4440, which keeps only the part that was load-bearing. Agreed with the review point: re-deriving "which installed automation belongs to this lane" on every turn duplicated authority that update-time reconciliation already owns, and multi-claim ambiguity is exactly what produced the original mis-install. #4440 drops the per-turn observation module, the scheduler-hint parameter plumbing, and the per-turn store scan. What it keeps is the gap this PR identified: update time can only report a pending migration while the App is running, and that report dies with the command's stdout. #4440 records the reviewed prompt-only request per lane at update time and projects only that recorded obligation each turn, so nothing re-classifies installed automations and a satisfied record stops projecting itself. |
Problem
loopx update applyreconciles the installed Codex App automation body only at update time. When the App is running, the offline adapter refuses and the update completes withupgrade_complete=false— and nothing in the recurring turn contract re-observes the installed body afterwards. A paused, resumed or re-activated heartbeat lane therefore keeps applying the policy of the day it was installed, silently, and the only report of it was the one-shot reconciliation output.Change
The live turn contract now carries the observation.
quota should-runreads this lane's installed automation once per turn and projects it asscheduler_hint.app_automation.prompt_binding. When the installed body is not the current managed loader, the packet hands the host one prompt-onlyapi_update_request(host_action=adopt_managed_bootstrap) that preserves the automation's name, status, RRULE and thread binding, so adoption stays explicit, non-blocking and separate from cadence handling. The read is bounded, read-only and fail-open.Right-sized revision
This revision replaces the earlier classification-every-candidate version with a smaller one:
should_run_packet.pyno longer carries its own discovery helpers; both reads (cadence state and installed prompt body) now live in one sharedapp_automation_observation.py, which is also why the file ends up below its maintainability budget again. No new ratchet exception is needed.load_codex_app_automation_manifestinstead of a private TOML walk, and picks the automation bound to the current thread, so a host with several automations claiming one Goal/agent no longer aborts the lane asambiguous; only a body both stores confirm is classified, and unconfirmed look-alikes are named only when nothing else is confirmable.installed_prompt_update.pynow calls the sharedautomation_update_requestbuilder instead of duplicating the request shape.currentnoradoption_required(unmanaged Goal/agent, disagreeing stores) reports its own status and never hands the host a request to apply. This path is covered by a regression test after it was found to raise during review of the compressed version.Validation
pytest tests/control_plane/test_automation_prompt_upgrade.py tests/control_plane/test_codex_app_prompt_binding_projection.py— 57 passed.tests/canary/test_maintainability_ratchet.py— 8 passed (the earlier CI blocker is resolved by shrinkingshould_run_packet.py, not by an exception).python -m mypy— Success: no issues found in 22 source files;ruff checkon the touched files — clean.examples/control_plane/heartbeat-prompt-smoke.pyandexamples/control_plane/cli-output-budget-regression-smoke.py— ok.loopx canary premerge --from-git-diff— all direct checks and all risk-profile checks pass, public/private boundary scan ok; the single catalog-canary failure isexamples/install-local-smoke.py, which fails identically on cleanorigin/main(unrelated pre-existing main-side failure).status=currentbound to the correct automation instead ofambiguous.Boundaries
The observation is read-only: it grants no delivery permission, no scheduler authority and no write to the App. The proposed request is prompt-only and preserves every field the App requires; adoption remains an explicit host decision. The observation never retargets another Codex home, registry, runtime root or CLI binary.