Skip to content

fix(scheduler): apply the heartbeat receipt fence to Python follow-ups - #8606

Draft
GZY-SUPER-HACKER wants to merge 1 commit into
loopx-project:mainfrom
GZY-SUPER-HACKER:fix/scheduler-stale-receipt
Draft

GZY-SUPER-HACKER wants to merge 1 commit into
loopx-project:mainfrom
GZY-SUPER-HACKER:fix/scheduler-stale-receipt

Conversation

@GZY-SUPER-HACKER

Copy link
Copy Markdown
Contributor

Goal And Delivered Outcome

  • Outcome basis / optional anchor: Related to [Bug]: the Python scheduler follow-up route accepts a heartbeat receipt a newer Turn superseded #8605 (the reproduced defect opened in parallel with this draft; the issue carries the same reproduction and the alternative fix shape). It does not close the broader compatibility-contract question in [Task]: Decide the support contract for receipt-less scheduler ACK/failure before retiring the Python facade #7876.

  • Goal/source and gap: a scheduler follow-up that carries a Turn id is receipt-bound by contract. The native transaction rejects both a missing and a superseded heartbeat receipt (loopx/control_plane/scheduler/heartbeat_followup.ts:338-356; the stale case is re-checked under the state lock in loopx/control_plane/scheduler/heartbeat_commit.ts:827-849). The Python compatibility route implemented only the first half: build_scheduler_followup_payload refused when the receipt was absent, then called record_quota_scheduler_ack, whose commit transport hard-coded turn_instance_id: null and require_heartbeat_receipt: false (loopx/control_plane/quota/scheduler_ack.py:193-194). The fence therefore never ran on that route.

  • Observable before → after, with the validation row that proves it: with a Turn whose receipt a later quota should-run superseded, quota scheduler-ack-current --turn-instance-id <old> --execute used to return rc 0, ok: true, scheduler_state_mutated: true and rewrite last_applied_rrule. It now returns rc 1, status: heartbeat_receipt_stale, state: blocked_receipt, scheduler_state_mutated: false, and leaves the scheduler state file absent (validation rows 1–2).

  • Issue/task and intended base: Related to [Bug]: the Python scheduler follow-up route accepts a heartbeat receipt a newer Turn superseded #8605; intended base main.

Author Declaration

  • Written by: model_agent — Claude Opus 5 (Anthropic), working under the direction and review of a human operator.

Implemented against

  • Specification and revision: the receipt contract already shipped in the current tree — loopx/control_plane/scheduler/heartbeat_followup.ts (heartbeatReceiptStatus gate and the structured receiptFailure payload) and its pinned tests tests/control_plane_ts/scheduler_heartbeat_followup.test.ts:184-255 (missing or superseded heartbeat receipt rejects before scheduler mutation, and the lock-recheck case). No new invariant is introduced; the change makes the Python route delegate to that existing owner.

  • Criteria:

Criterion (spec clause) Disposition Symbol / path Test or command
A Turn-bound follow-up rejects a missing receipt implemented (unchanged, Python pre-check retained) loopx/cli_commands/quota_scheduler_followup.py existing behaviour; tests/control_plane/test_scheduler_stale_receipt_gate.py control in the issue reproduction
A Turn-bound follow-up rejects a superseded receipt implemented _run_native_followup now forwards turn_instance_id and sets require_heartbeat_receipt tests/control_plane/test_scheduler_stale_receipt_gate.py::test_superseded_receipt_cannot_commit_a_scheduler_ack
A rejected follow-up does not mutate scheduler state implemented native receiptFailure returns before the commit same test (scheduler_state_mutated: false, state file absent)
A fresh receipt still commits implemented same transport tests/control_plane/test_scheduler_stale_receipt_gate.py::test_fresh_receipt_still_commits_the_ack
Callers without a Turn keep the receipt-less compatibility request implemented _run_native_followup(turn_instance_id=None) default the in-process record_quota_scheduler_ack(...) path is unchanged
  • Self-check before submission: I read both routes end to end (CLI payload builder, the public ACK API, the transport, and the TS gate), then reproduced the defect on current main before changing anything. The fix does not add a second copy of the staleness rule: it hands the Turn to the transaction that already owns it, and keeps the Python absence check because it prevents rebuilding authority from the live frontier. I verified the counter-experiment (product files reverted, tests kept): the superseded test fails with the old ok: true result while the fresh-receipt twin still passes.

Scope And Continuation

  • Completed scope and remaining work: complete within this scope for scheduler-ack, scheduler-ack-current and scheduler-fail-current, which share the transport. The broader question — whether receipt-less manual commands should remain supported at all, and whether the compatibility facade should be retired — stays with [Task]: Decide the support contract for receipt-less scheduler ACK/failure before retiring the Python facade #7876 and is not decided here.

  • Unrelated but visible: the Python side still performs its own existence check before rebuilding the decision. That duplicates the existence half of the fence, but it fails earlier with the wording the issue's control case relies on; removing it would change the missing-receipt payload, so it is left alone.

  • Slice boundary / successor: independently testable and reversible — one optional argument threaded through four call sites plus one regression module. Reverting the four product lines restores the old behaviour. No successor is required.

Validation

  • Tested revision: 5a84086a6
  • Run state: finished
  • Input classes: synthetic
Check kind Result Public-safe evidence / limitation
unit passed tests/control_plane/test_scheduler_stale_receipt_gate.py — 2 passed: a superseded receipt is refused with heartbeat_receipt_stale and no scheduler state file; a fresh receipt still commits
regression_parity passed With the four product lines reverted and the tests kept, the superseded test fails (rc 0, ok: true) and the fresh-receipt twin passes; with the change restored, both pass
real_entrypoint passed Both tests drive the real loopx CLI through loopx.control_plane.testing.canary_harness.run_json_cli_result; the suite uses the repository's own write_cli_fixture
static passed ruff check clean on the three product files and the new test; git diff --check clean
real_backend not_applicable No live Codex App automation was installed or triggered; the defect is in the local receipt/commit boundary

Frontend / Visual Evidence

  • UI impact: none — CLI follow-up control plane only.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Refactoring (no functional changes)
  • Documentation update
  • Test update

LoopX Area

  • Control plane (goals, todos, quota, scheduler, registry, runtime)
  • Benchmark boundary (adapters, runners, verifiers, scoring, evidence)
  • Capability or extension (providers, adapters, skills)
  • Public docs or presentation surface (README, protocols, dashboard)
  • Build, packaging, installer, or CI
  • Host or runtime integration

Technical Direction

  • Direction / acceptance reference, when applicable: S4/S10 reliability diagnostics (scheduler receipt honesty). No new roadmap id is claimed; the issue is the anchor.

Shared-authority RFC fixture impact

  • Production-scale fixture schema: N/A
  • Semantic dimensions changed, or reviewed no-impact rationale: N/A
  • Provider conformance arms run: N/A
  • Read-only legacy/file/PostgreSQL three-arm rehearsal: N/A (no runtime-routing, promotion or compatibility-projection change)

Boundary Checklist

  • Neither the diff nor this PR body/comments/attachments disclose private state, credentials, raw traces or verifier output, internal links, or local machine paths (including .loopx/, .codex/goals/, and live ACTIVE_GOAL_STATE.md).
  • I did not duplicate maintainer-owned benchmark work unless a maintainer split out a public issue for it.
  • I kept the change scoped to the linked issue/task.
  • Existing English/Chinese RFC pairs (including ledger entries) both have a diff, and the local premerge check python3 scripts/check_rfc_bilingual.py --base origin/main --head HEAD passes, or no paired RFC changed.
  • I completed the visual evidence section for UI changes, or marked UI impact none.
  • Every commit includes a DCO Signed-off-by trailer (git commit -s).

A Turn id already bound the follow-up to one heartbeat receipt, but the compatibility transport sent turn_instance_id=null and require_heartbeat_receipt=false to the native transaction. The Python route therefore rejected a missing receipt and accepted a superseded one, while the generated route rejects both.

Forward the Turn id when the caller has one, so the native transaction owns the missing/superseded decision for both routes. Callers without a Turn (the in-process API) keep the receipt-less compatibility request.

Refs loopx-project#8605

Signed-off-by: GZY-SUPER-HACKER <162807803+GZY-SUPER-HACKER@users.noreply.github.com>

This branch has not been deployed

No deployments
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