feat(lease): make the author task heartbeat load-bearing (Closes #790, Slice A) #794
Open
jcwalker3
wants to merge 1 commits from
fix/issue-790-slice-a-heartbeat-policy into master
pull from: fix/issue-790-slice-a-heartbeat-policy
merge into: :master
:master
:feat/issue-798-worker-registry-schema
:feat/issue-610-live-remote-parity
:docs/issue-632-web-console-architecture
:feat/issue-628-autonomous-handoffs-orchestration
:fix/issue-790-slice-a-heartbeat-policy
:fix/issue-760-exact-owner-renewal
:fix/issue-787-kill-segment-separators
:chore/issue-681-preserve-review-session-wip
Labels
Clear labels
allocator
anti-stomp
architecture
bug
chore
codex
concurrency
contamination
control-plane
dashboard
database
design
documentation
enhancement
gitea
glitchtip
important
incident
incident-bridge
integration
jenkins
labels
leases
mcp
mcp-health
mcp-menu
multi-project
mutating
nice-to-have
observability
portability
preflight
protected-branch
queue
read-only
reconnect
recovery
refactor
release
reliability
resumable-review
reviewer
roadmap
safety
security
self-hosted
sentry
stale-runtime
status:blocked
status:in-progress
status:pr-open
status:ready
terminal-lock
testing
tracker
type:bug
type:feature
type:feature
type:guardrail
visibility
workflow
workflow-hardening
workflow-hardening
Controller-owned work allocator
Prevent concurrent LLM session stomping
Architecture / structural design
OpenAI Codex client / workflow session surface
Concurrent session safety
Workflow or session contamination incident
MCP control-plane coordination and allocation authority
MCP operational dashboard/queue view
Internal coordination storage (SQLite/Postgres)
Design / investigation, no implementation
Docs / runbooks
New feature or improvement
Gitea MCP workflow
GlitchTip integration
Operational or process incident requiring durable audit trail
Sentry-to-Gitea incident bridging
Integration testing
Jenkins integration
Label taxonomy management
Lease adopt/release/expire lifecycle
MCP server / tooling
MCP namespace and runtime health
MCP menu surface
Work spanning multiple monitoring projects or Gitea repos
Mutating action; requires gating
Observability, metrics, traces, error reporting
Cross-platform / portability
Shared preflight gates before mutation
Protected branch / stable-branch policy concern
Work queue visibility and allocation
Read-only, no mutation
MCP client reconnect/reload recovery path
Recovery paths for stale/foreign leases
Code refactor / restructure
Release / versioning
Reliability / failure handling
Persist and resume prepared review verdicts across sessions
Reviewer workflow tooling
Roadmap / umbrella issue
Safety rails and fail-closed mutation guards
Security / trust boundary
Self-hosted infrastructure integration
Sentry error monitoring integration
Stale backend daemon / runtime-vs-master parity failures
Issue is blocked
Issue is being worked on
Issue has an open pull request
Issue is ready for work
Terminal review lock (#332) path
Tests / test coverage
Issue tracker hygiene / meta
Bug or defect
Feature or enhancement
Feature or enhancement
Safety gate or guardrail
Workflow state visibility for LLMs/operators
Cross-tool workflow
LLM workflow coordination hardening
LLM workflow coordination hardening
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: Scaled-Tech-Consulting/Gitea-Tools#794
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #790
Follow-up: #792
Follow-up: #793
This pull request implements Slice A only
Issue #790 was reduced to Slice A by the scope transfer recorded in its comment 14010, so the closing reference above is accurate. The rest of the original scope was moved to dedicated issues before this pull request was opened:
lock_generationverification inverify_lock_for_mutation, the Issue #760 renewal re-scope, remaining task-class policy alignment, control-plane lease alignment.Merging this closes #790 correctly. It does not complete #792 or #793, and #790 should not be reopened to represent them.
Problem
issue_lock_store.assess_lock_freshnessparsedlast_heartbeat_atand then never consulted it. Liveness was decided by an absolute four-hourexpires_atand by process-identifier liveness, and the recorded identifier is the long-lived MCP daemon rather than the authoring task, so an abandoned claim stayed live for the full four hours. A tree-wide search found the field written in exactly one place,gitea_mcp_server._build_author_issue_work_lease, and advanced by nothing anywhere.Issue #787 / PR #789 hit this. Issue #760 / PR #791 hit it again: its lease was minted at
04:19:08Z, never heartbeated, its recorded pid 39849 stayed alive as a daemon, and reconciliation was still blocked at06:39:43Z— more than five hours after the work landed on master.What changed
A1 — one authoritative policy source. New
lease_policydeclares every duration for every task class: author initial and sliding TTL 10 minutes, heartbeat cadence 2, stale warning 5, missed-heartbeat grace 10, absolute cap 8 hours, recovery grace 10, terminal race-drain 2. It ships first so the first heartbeat and TTL behavior to run reads from it (AC-N7). The duplicated four-hour literal is deleted from bothissue_lock_storeandgitea_mcp_server. Reviewer, merger, and conflict-fix classes are declared but not rewired; a test asserts the declared numbers still match the constantsreviewer_pr_leaseandpr_work_leaseown, so the two cannot drift apart before #793 migrates those call sites.A2 — load-bearing freshness, with two intentional asymmetries. An alive process identifier never establishes freshness anywhere (AC-N2): it is recorded as evidence and no branch returns live because of it. A dead one still marks a lease stale, and that band still precedes every heartbeat evaluation, so Issue #753 dead-session recovery keys on exactly the classification it always did. New bands
stale_missed_heartbeatandstale_absolute_capare classified inbranch_cleanup_guardinstead of falling through to unknown-status, and still block unless the ownership record provesreclaim_allowed is True. A heartbeat lease carrying no heartbeat is contradictory and fails closed.assess_expired_lock_reclaimaccepts a lapsed heartbeat as reclaim grounds for heartbeat-lifecycle leases only — under this lifecycle the heartbeat is the liveness proof, and additionally demanding a dead process would reinstate the original defect.A3 and A4 — task-session identity and the writer.
mint_task_session_idproduces an ownership key containing no process identifier, because the daemon identifier is reused by every task it serves and identifies none of them.heartbeat_session_lockwrites inside the existing per-issue flock under the Issue #772 generation compare-and-swap, verifying exact issue, branch, realpath-normalized worktree, claimant username, claimant profile, and recorded session identifier. It cannot acquire, take over, or revive: a lease past its grace is refused and must use the reclaim path, so a session that stopped proving liveness cannot restore ownership retroactively. Newgitea_heartbeat_issue_lockgates on the authoritylock_issuealready requires, being a strictly narrower operation.A5 — legacy compatibility (AC-N8). An explicit
lifecycle_versionmarker, never a timestamp comparison, discriminates legacy from heartbeat leases. A legacy lock haslast_heartbeat_atmatchingcreated_atforever precisely because nothing advanced it, and a freshly minted heartbeat lease has them matching too, so that equality carries no information in either direction. Legacy locks keep their recorded absolute expiry and are never evaluated against the short grace, so deploying this cannot make an existing claim instantly reclaimable. They leave that state only through terminal retirement (#792) orrebind_legacy_lock, which re-verifies the exact owner and mints a genuine identifier and first heartbeat while preserving the original claim underlegacy_origin. Rebinding a lapsed legacy lease is refused; that case belongs to Issue #760 renewal or Issue #601 reclaim.A6 — native coverage. Review #499 proved assessor-level tests miss discard points, so
tests/test_issue_790_heartbeat_mcp_path.pydrives the real tools against a real git repository and a real durable lock.Files changed
lease_policy.pyissue_lock_store.pyheartbeat_session_lock,rebind_legacy_lockgitea_mcp_server.pygitea_heartbeat_issue_locktoolbranch_cleanup_guard.pytask_capability_map.pyheartbeat_issue_lockcapability entrydocs/mcp-tool-inventory.mdtests/test_issue_790_lease_policy.pytests/test_issue_790_heartbeat_mcp_path.py1974 insertions, 32 deletions.
Non-regressions preserved
Issue #760 exact-owner renewal is untouched and stays in force until #793 replaces it end to end. Issue #753 dead-session recovery, Issue #755 owning-pull-request evidence, Issue #772 generation compare-and-swap, and Issue #747 reviewer sliding TTL all keep their existing behavior, each covered by its own suite below.
Tests and results
Run with
venv/bin/python -m pytestfrom the allocated worktree.test_issue_790_lease_policy.py40,test_issue_790_heartbeat_mcp_path.py15).test_issue_lock_store,test_lease_lifecycle,test_issue_753_dead_pid_lock_recovery,test_issue_755_owning_pr_recovery,test_issue_760_exact_owner_lease_renewal,test_issue_760_mcp_renewal_path,test_issue_768_strict_descendant_recovery,test_issue_772_unpublished_claim_recovery,test_lock_issue_mcp_registration,test_issue_lock_worktree,test_issue_lock_adoption,test_issue_work_duplicate_gate,test_branch_cleanup_guard,test_task_capability_role_invariants,test_issue_claim_heartbeat,test_worktrees) — 383 passed, 98 subtests passed.Baseline comparison: a clean detached worktree at
branches/baseline-master-issue-790, pinned to master620ed6e9a9550b8da2ceb82d9ab8744e8920490f, reports 11 failed, 4240 passed, 6 skipped, 493 subtests passed running the identical command. The eleven failing node identifiers are byte-for-byte the ones on this branch: six intest_commit_payloads.py, two intest_issue_702_review_findings_f1_f6.py, and one each intest_mcp_server.py::TestPreflightVerification,test_post_merge_moot_lease.py, andtest_reconciler_supersession_close.py. None touch lease, lock-freshness, or heartbeat code. The 55-test delta between 4240 and 4295 is exactly the new suites, so this branch adds no new failures. Both worktrees reported a zero-entrygit status --porcelainbefore and after validation.Risk
Behavioral change to the author lock lifecycle, so the risk concentrates in two places and both are covered.
Shortening the author TTL from 240 to 10 minutes could in principle make deployed claims reclaimable at restart. It cannot: every lock already on disk lacks the lifecycle marker and is therefore held to its recorded absolute expiry, and the short grace applies only to leases minted after this lands.
Loosening reclaim could in principle weaken a guard. The new reclaim ground is reachable only for heartbeat-lifecycle leases, never for a legacy lock, and every other precondition — cleanliness, ancestry, publication, worktree binding, the duplicate-work gate, the compare-and-swap — is unchanged.
One consequence worth stating plainly for review: an author session that runs longer than 10 minutes without calling
gitea_heartbeat_issue_lockwill have its claim become reclaimable. That is the intended behavior, and it is why the heartbeat cadence is 2 minutes, giving five missed beats before the grace lapses.Known limitations
The heartbeat is a tool an author session must call; nothing in this slice schedules it automatically. Reviewer, merger, conflict-fix, and control-plane leases still run their own lifecycles until #793.
verify_lock_for_mutationstill does not check the generation, which is also #793.Provenance
fix/issue-790-slice-a-heartbeat-policy/Users/jasonwalker/Development/Gitea-Tools/branches/issue-790-slice-a-heartbeat-policy243f52dc7959c362242ebea321fafa3950f13083620ed6e9a9550b8da2ceb82d9ab8744e8920490fCTH: Author Handoff
Status: published_awaiting_independent_review
Next owner: reviewer (prgs-reviewer / sysadmin)
Current blocker: none
Decision: PR #794 implements Slice A of #790 only; ready for independent review
Proof: head
243f52dc7959c362242ebea321fafa3950f13083onfix/issue-790-slice-a-heartbeat-policy; base master; Closes #790; follow-ups #792/#793 are out of scopeNext action: independent formal review at exact head
243f52dc7959c362242ebea321fafa3950f13083Ready-to-paste prompt: see NEXT_PROMPT below
Canonical Issue State
STATE: pr_open_awaiting_review
WHO_IS_NEXT: reviewer
NEXT_ACTION: formal independent review of PR #794 at head
243f52dc79NEXT_PROMPT:
WHAT_HAPPENED: Canonical author handoff posted for already-published PR #794 (incomplete handoff). Implementation and validation evidence are in the PR body.
WHY: Queue hygiene — open PR lacked CTH handoff so reviewer ownership was unclear.
RELATED_PRS: #794 (this PR); follow-ups #792, #793
BLOCKERS: none
VALIDATION: See PR body — new suites 55 passed; lock/lease regression 383 passed; full suite 4295 passed / 11 baseline fail vs master 4240/11.
LAST_UPDATED_BY: jcwalker3 (prgs-author)
REPOSITORY: Scaled-Tech-Consulting/Gitea-Tools
ISSUE: #790
PR: #794
HEAD_SHA:
243f52dc79BASE_BRANCH: master
ACTING_ROLE: author
ACTING_IDENTITY: jcwalker3
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #794
issue: #790
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 89687-276bb9edcbbf
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr-794
phase: claimed
candidate_head:
243f52dc79target_branch: master
target_branch_sha:
620ed6e9a9last_activity: 2026-07-22T10:52:21Z
expires_at: 2026-07-22T11:02:21Z
blocker: none
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #794
issue: #790
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 50914-eb6bb39eb08a
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr-794
phase: claimed
candidate_head:
243f52dc79target_branch: master
target_branch_sha: none
last_activity: 2026-07-22T11:05:47Z
expires_at: 2026-07-22T11:15:47Z
blocker: none
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #794
issue: #790
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 50914-eb6bb39eb08a
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr-794
phase: validation
candidate_head:
243f52dc79target_branch: master
target_branch_sha: none
last_activity: 2026-07-22T11:07:43Z
expires_at: 2026-07-22T11:17:43Z
blocker: none
Canonical PR State
STATE: changes-requested
WHO_IS_NEXT: author
NEXT_ACTION: Fix assess_same_issue_lease_conflict so heartbeat-lifecycle non-live bands (stale_missed_heartbeat, stale_absolute_cap) enter the reclaim path even when expires_at is still in the future; add regression tests; re-request review.
NEXT_PROMPT:
WHAT_HAPPENED: Independent review of PR #794 (Slice A author heartbeat policy) by sysadmin/prgs-reviewer.
WHY: assess_expired_lock_reclaim allows reclaim for the new stale bands, but assess_same_issue_lease_conflict still keys foreign reclaim on is_lease_expired (expires_at only). Absolute-cap with a recent heartbeat, and TTL>grace env overrides, leave foreign reclaim blocked as active.
ISSUE: #790
HEAD_SHA:
243f52dc79REVIEW_STATUS: REQUEST_CHANGES
MERGE_READY: no
BLOCKERS: conflict gate does not honor heartbeat-lifecycle non-live reclaim bands when expires_at is still future
VALIDATION: pytest test_issue_790_lease_policy + test_issue_790_heartbeat_mcp_path = 55 passed; lock/lease/#753/#760/branch_cleanup/capability regression = 173 passed; manual probes confirmed absolute-cap and TTL>grace conflict-gate hole
LAST_UPDATED_BY: sysadmin (prgs-reviewer)
Independent review detail
Verdict: REQUEST_CHANGES
One correctness hole in the acquisition conflict gate relative to the new freshness/reclaim semantics. Everything else in Slice A looks solid.
What looks good
Default-path missed-heartbeat reclaim works when expires_at and grace align (defaults 10/10).
Blocking: conflict gate still keys off expires_at only
assess_expired_lock_reclaim allows reclaim for stale_missed_heartbeat and stale_absolute_cap without a dead PID.
assess_same_issue_lease_conflict only enters that reclaim branch when is_lease_expired (expires_at only). When freshness is non-live and expires_at is still future, foreign acquisition is rejected as active.
Reproduced on this head:
assess_foreign_lock_overwrite already uses is_lease_live; the same-issue path is inconsistent. gitea_lock_issue raises before bind.
Suggested fix: for heartbeat-lifecycle leases, enter reclaim/renewal when not is_lease_live (or status in the new bands), not only when is_lease_expired. Keep legacy on absolute expires_at. Add regression tests for the three cases above.
Non-blocking
Happy to re-review once the conflict-gate wiring matches the new reclaim bands.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.