Skip to content

fix(heartbeat): observe the installed automation prompt binding every turn - #4425

Closed
huangruiteng wants to merge 1 commit into
mainfrom
codex/automation-prompt-binding
Closed

huangruiteng wants to merge 1 commit into
mainfrom
codex/automation-prompt-binding

Conversation

@huangruiteng

@huangruiteng huangruiteng commented Sep 15, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

loopx update apply reconciles the installed Codex App automation body only at update time. When the App is running, the offline adapter refuses and the update completes with upgrade_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-run reads this lane's installed automation once per turn and projects it as scheduler_hint.app_automation.prompt_binding. When the installed body is not the current managed loader, the packet hands the host one prompt-only api_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.py no longer carries its own discovery helpers; both reads (cadence state and installed prompt body) now live in one shared app_automation_observation.py, which is also why the file ends up below its maintainability budget again. No new ratchet exception is needed.
  • Discovery reuses the existing load_codex_app_automation_manifest instead 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 as ambiguous; only a body both stores confirm is classified, and unconfirmed look-alikes are named only when nothing else is confirmable.
  • installed_prompt_update.py now calls the shared automation_update_request builder instead of duplicating the request shape.
  • A confirmed body whose classification is neither current nor adoption_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.
  • Wider set (scheduler execution context/fallback hint, quota CLI projection, effect-turn live quota decision, heartbeat prompt support, host prompt behavior, delivery continuity, host bootstrap lifecycle) — 311 passed.
  • tests/canary/test_maintainability_ratchet.py — 8 passed (the earlier CI blocker is resolved by shrinking should_run_packet.py, not by an exception).
  • python -m mypy — Success: no issues found in 22 source files; ruff check on the touched files — clean.
  • examples/control_plane/heartbeat-prompt-smoke.py and examples/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 is examples/install-local-smoke.py, which fails identically on clean origin/main (unrelated pre-existing main-side failure).
  • Real-host readback with this machine's automation store (a lane that has three automations claiming the same Goal/agent, two of them frozen legacy bodies) returns status=current bound to the correct automation instead of ambiguous.

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.

… 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>
@huangruiteng

Copy link
Copy Markdown
Collaborator Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant