feat(fleet): add CAS-protected stale worker retirement capability #980
Open
opened 2026-07-30 10:49:39 -05:00 by jcwalker3
·
5 comments
No Branch/Tag Specified
master
fix/issue-983-cross-repo-base-ref
feat/issue-980-stale-worker-retirement
fix/issue-975-client-identity-heartbeat
fix/issue-973-cross-repo-canonical-roots
fix/issue-970-safely-resolve-missing-worktrees
fix/issue-969-native-mcp-bootstrap
feat/issue-664-break-glass-restart
feat/issue-708-mcp-namespace-attachment
feat/issue-665-restart-audit
fix/issue-700-durable-walls
fix/issue-704-prevent-env-workspace-bindings
feat/issue-707-cross-project-boundaries
fix/issue-690-review-profile-switch-guard
fix/issue-953-bootstrap-lock-provenance
feat/issue-949-native-fleet-inventory
fix/issue-943-runtime-context-helpers
fix/issue-945-owning-pr-renewal-evidence
fix/issue-941-scope-guard-bootstrap-wiring
docs/issue-930-remote-mcp-coupling-inventory
fix/issue-892-author-bootstrap-deadlock
fix/issue-686-detect-reject-manual-mcp
fix/issue-672-mcp-config-drift
fix/issue-689-deterministic-mcp-namespace
feat/issue-666-concurrent-mcp-restart-tests
feat/issue-659-maintenance-drain-mode
feat/issue-648-notifications-console
fix/issue-670-direct-master-incident
feat/issue-644-console-recovery
feat/issue-650-providers-insights
feat/issue-669-scoped-component-recovery
docs/issue-668-mcp-ha-rolling-restart
feat/issue-667-console-restart-controls
feat/issue-645-linkage-console
feat/issue-643-request-preview-initiate
fix/issue-897-permission-stale-runtime-classification
feat/issue-641-runtime-session-view
feat/issue-663-restart-classes
feat/issue-661-drain-proof-hard-gate
fix/issue-854-semantic-container-exclusion
issue-640
fix/issue-682-starlette-httpx2
v1.1.0
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
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#980
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.
Summary
Add a sanctioned controller/reconciler mutation capability that can safely retire conclusively stale MCP worker-registry entries using a dry-run-first, compare-and-swap-protected workflow.
The fleet snapshot introduced through issue #978 and merged PR #979 exposes authoritative worker state, blockers, identity provenance, and fleet revision tokens. However, the control plane currently has no native mutation operation capable of retiring stale worker rows.
Observed live state
At master
108cbfa173de34470d7abc022b929e6b3adc418a, an authoritative fleet assessment found:stale_orphaned_workerfindings.legacy_incomplete_identityblockers.Capability probes produced:
reconcile_instance_fleet→unknown_task,stop_required:truecleanup_stale_workers→unknown_task,stop_required:truegitea_snapshot_instance_fleet→ explicitly read-onlygitea_reconcile_after_restart→ read-only and limited to session/lease reconciliationBecause no CAS-protected apply operation exists, the stale entries cannot be retired through sanctioned tools. Direct database manipulation is prohibited.
The current fleet snapshot is observational and is not proven to gate the ordinary author workflow (including branch, commit, push, or PR operations, PR #906 remediation, issue #650 work, or other unrelated authorized mutations). This issue addresses stale worker-registration accuracy and safe retirement on its own merits, and does not depend on #981, BAA, PR #906, or issue #650.
Required behavior
Implement a canonical native fleet-reconciliation capability available only to authorized controller/reconciler profiles.
The workflow must support:
Authoritative dry run
registry_revisionorregistry_fingerprint).Strict candidate eligibility
A worker may be retired only when the control plane conclusively establishes that it is a stale orphan, including:
pid_alive:falseownership_state:staleCAS-protected application
registry_revisionorregistry_fingerprint, returned by the plan operation.snapshot_at, wall-clock time, request time, report-generation time, or any other volatile observation metadata.registry_revision_movedor equivalent result.fleet_revisionmust not be used for this CAS unless its semantics are first changed to satisfy this stable contract.Fail-closed safety
Stop without mutation if:
Idempotency and auditability
Post-apply verification
Capability and schema integration
Important separation of concerns
This issue must not solve incomplete live-worker identity by inventing or backfilling trusted IDs.
The two observed live workers used
legacy-pid-*identities and must remain untouched by stale-worker retirement. Trustedclient_instance_idpropagation through launchers is a separate enrollment problem.Therefore, successfully retiring the 88 stale rows may produce:
stale_worker_count: 0legacy_incomplete_identityblockerslive_fleet_safe:falseThat is an acceptable and truthful result for this issue. The operation must never report complete fleet safety while independent live-identity blockers remain.
Non-goals
Acceptance criteria
registry_revisionorregistry_fingerprint) and candidate fingerprint.Related work
Canonical Issue State
STATE: needs-revision — R3-C; no external dependency remains, two body/AC defects block a clean author cycle
WHO_IS_NEXT: author
NEXT_ACTION: prgs-author applies the smallest revision below to this issue body, then claims and implements #980 through the ordinary issue-author workflow. No dependency on #981, on a BAA/first-install mechanism, on PR #906 or #650, or on trusted launcher identity remains. Labels unchanged: the live catalog has neither
status:needs-revisionnorstatus:needs-triage.NEXT_PROMPT:
WHAT_HAPPENED:
Disposition R3-C — goal valid, acceptance criteria require a small correction
Master SHA (parity-verified twice):
108cbfa173de34470d7abc022b929e6b3adc418aController preflight (this session): namespace
gitea-controller; identitysysadmin; profileprgs-controller; declared rolecontroller, inferredrole_kindreconcilerin runtime context; bindingprgstoScaled-Tech-Consulting/Gitea-Tools; startup, runtime, local and live remote head all108cbfa173de;in_parity:true;live_stale:false; parity-layermutation_safe:true; control checkout clean onmasterincluding untracked files (git status --porcelain --untracked-files=allempty);gitea.readandgitea.issue.commentallowed, label mutation not exercised.Recorded dependencies — none. The #980 body names #978/PR #979 (merged prerequisite, already landed), PR #906/#650 (a downstream consumer, not a prerequisite), and trusted
client_instance_idpropagation, which it explicitly places out of scope. It does not name #981 or any BAA/first-install mechanism.gitea_list_dependency_edgesreturns zero durable edges with #980 as either source or target. #980 carries no comments prior to this one.Effect of #981 comment 18781 (D2-A). The pause on #980 was inherited entirely from the superseded fleet-deadlock premise of comment 18775. That premise was independently re-verified as unsupported at this revision:
live_fleet_safeis produced atmcp_fleet_snapshot.py:804and has zero non-test readers;mcp_fleet_snapshotis imported by exactly one server surface,gitea_snapshot_instance_fleet(gitea_mcp_server.py:19461), which is controller/reconciler-restricted, mapped togitea.readintask_capability_map.py:170,174, and returnsmutation_performed:falsewith an explicit read-only permission scope. No ordinary author mutation path consumes a fleet-derived gate. The #981-derived pause on #980 is therefore released.Author-workflow capability map at this revision. Every operation #980 needs is configured and routes to
prgs-author:work_issue->gitea.pr.create;lock_issue->gitea.issue.comment;create_branch->gitea.branch.create;commit_files->gitea.repo.commit;push_branch->gitea.branch.push;create_pr->gitea.pr.create;comment_issueandmark_issue->gitea.issue.comment. Claim release runs throughgitea_mark_issue/lease release rather than a distinctrelease_issue_claimtask key. No author mutation was attempted as a test in this session.Fleet-layer versus parity-layer gates are distinct and were not conflated. Live snapshot at
108cbfa173de,registry_revisionfleetrev-3d8c6d1c4b8a3ab8: 95 registered workers, 1 live, 94 stale, 0 historical; all 94 stale rows carryliveness.pid_alive:falsewith none unprobeable or alive; blockers are 94stale_orphaned_workerwarnings plus 1legacy_incomplete_identity; no duplicate namespace worker, no identity collision, no foreign repository, no ambiguous candidate;live_fleet_safe:falseand fleet-layermutation_safe:falsewhile parity-layermutation_safe:true. The fleet-layer value is observational and gates nothing on the author path.Prior implementation — primitives only, no duplicate.
WorkerRegistry.release(mcp_worker_identity.py:1192) idempotently marks a registrationreleasedand never deletes history, but it has zero non-test callers, no MCP tool, no CAS, no eligibility gate, no role restriction and no audit event. Classification inputs already exist:ownership_statevalueslive/stale/historical/live_untrusted_identity/orphaned(mcp_fleet_snapshot.py:331-337), thestaleflag (line 377), andWorkerRegistry.is_live(mcp_worker_identity.py:673-717), in whichpid_alivecan only withdraw liveness and never grant it. No open or merged PR implements worker-registration retirement.Adjacent open PR #971 (#969) is not a duplicate. Its branch head
e8bae606touchesworker_registrationsin zero lines; it retires rows in the control-planesessionstable via a newsession_lifecyclemodule andgitea_retire_stale_workflow_sessions. That is the registry #980's own non-goals exclude. It is a useful precedent for CAS retirement and PID-reuse handling, and it is a textual conflict risk only: it also editscontrol_plane_db.py,gitea_mcp_server.pyandtask_capability_map.py, and it currently reportsmergeable:falseagainst master. This is ordinary merge-conflict risk, not a dependency.Defect 1, blocking as written. Requirement 3 demands "Require the exact fleet revision produced by the dry run" and "Abort without partial mutation if the fleet revision or candidate set changes", and the acceptance criteria demand both a successful apply of the unchanged plan and a failed apply on revision change. At this revision those cannot both hold.
mcp_fleet_snapshot._consistency_token(lines 271-294) seeds its digest withsnapshot_at, formatted at second precision by_ts(lines 104-105), andgitea_snapshot_instance_fleetnever passes the declaredregistry_revisionoverride (gitea_mcp_server.py:19598-19603), so the emitted token is time-seeded. Direct evaluation of the pure function with byte-identical rows one second apart yieldsfleetrev-fd8d1ca7c90315c3versusfleetrev-cc5da6c5dcff053b. Any dry-run-then-apply cycle spanning more than one second therefore aborts unconditionally. The issue's own "Observed live state" records exactly this drift as evidence of moving fleet state, which would mislead an implementer into treating a derivation artifact as real churn.Defect 2, obsolete premise in the body. "This blocks unrelated authorized work, including author remediation of PR #906 for issue #650" asserts the fleet-deadlock premise corrected by D2-A. Registry accuracy remains independently valuable, so the goal survives, but the stated motivation is recorded as fact and is false at this revision.
Everything else in #980 verified sound. The separation of concerns is correct: retirement needs no trusted launcher identity, and the issue's declared acceptable outcome of
stale_worker_count:0with residual livelegacy_incomplete_identityblockers andlive_fleet_safe:falsematches how the snapshot computes safety. The eligibility conjunction is strictly narrower than the snapshot's ownstaleflag: becausepid_alivecannot grant liveness, a heartbeat-lapsed but running process classifies asstale, yet #980's addedpid_alive:falserequirement preserves it, and an unprobeablepid_alive:nullalso fails closed. The issue distinguishes registered stale rows, live workers, duplicate processes within one cohort, and independent legitimate workers, and excludes live, foreign, conflicting and ambiguous entries from retirement. PID reuse is unaddressed but errs toward over-preservation.Do not do: implement #980 before the revision lands, activate or design the BAA/first-install lane, retire any worker row, touch #981, PR #906, #650, review 624, branch
feat/issue-650-providers-insights, or worktreebranches/conflict-fix-pr-906-983e8ac.WHY: #980 has no remaining external dependency — the #981/BAA pause is released and no fleet-derived gate touches the author path — but its CAS-on-fleet-revision requirement cannot be satisfied against the time-seeded revision token at
108cbfa173de, and its blocking rationale repeats a premise already corrected by #981 comment 18781, so a small body revision is required before an author cycle can close cleanly.RELATED_PRS: PR #979 (#978) merged and supplies the snapshot this issue builds on. PR #971 (#969) is open, retires control-plane session rows only, touches no
worker_registrationsline, and shares three files with #980's likely surface. PR #906 (#650) is untouched and is a consumer, not a prerequisite. No PR exists or was created for #980.BLOCKERS: Acceptance-criteria defect — compare-and-swap on the fleet revision token cannot pass because
mcp_fleet_snapshot._consistency_tokenis time-seeded andgitea_snapshot_instance_fleetnever supplies theregistry_revisionoverride. Obsolete premise — the PR #906 blocking rationale was superseded by #981 comment 18781 (D2-A). Both are internal to #980 and owned by prgs-author; neither is an external dependency. Labels could not move tostatus:needs-revisionorstatus:needs-triagebecause neither label exists in the live catalog. Unblock after the author revises requirement 3, its matching acceptance criterion, and the obsolete rationale sentence.VALIDATION: Controller-only read plus one issue comment, on a clean control checkout at
108cbfa173de;gitea_whoamithengitea_resolve_task_capability(comment_issue, issue_number=980)immediately before this post, withallowed_in_current_session:true,stop_required:false,mutation_performed:false. No author mutation was performed or tested; no code, branch, worktree, commit, push, PR, review, merge, claim, lease, grant, fleet-registry, database, configuration, session, credential or process change. Source claims were verified by read-only inspection of the control checkout at that SHA; the revision-token instability was reproduced by evaluating the pure_consistency_tokenfunction with synthetic rows and no database access. Issue #981, PR #906, issue #650, review 624, branchfeat/issue-650-providers-insightsand worktreebranches/conflict-fix-pr-906-983e8acwere not modified. Labels on #980 were left exactly as found.LAST_UPDATED_BY: sysadmin / prgs-controller (gitea-controller)
WHO_IS_NEXT: prgs-author to apply the R3-C minimal revision to #980, then claim and implement it through the ordinary issue-author workflowCanonical Issue State
STATE: ready — R3-C specification revision complete; issue #980 body updated with stable registry CAS contract and corrected obsolete rationale
WHO_IS_NEXT: author
NEXT_ACTION: Fresh prgs-author session claims and implements #980 through the ordinary issue-author workflow.
NEXT_PROMPT:
WHAT_HAPPENED:
Issue #980 body was revised under R3-C:
WHY:
R3-C review identified that snapshot_at in _consistency_token caused identical registry states observed at different times to produce different tokens, making CAS dry-run/apply impossible. Additionally, PR #906/#650 blocking premise was superseded by D2-A disposition of #981.
RELATED_PRS:
PR #979 (#978) merged prerequisite. PR #971 (#969) open adjacent PR. No PR exists for #980.
BLOCKERS:
None. Issue #980 is ready for ordinary author implementation.
VALIDATION:
Author-only read-after-write verification of issue #980 body edit at master
108cbfa173. Body verified preserved with both corrections present. State (open) and labels (status:ready) preserved intact. Clean control checkout at 108cbfa173de; zero code, branch, worktree, commit, push, PR, claim, lease, or configuration changes performed.LAST_UPDATED_BY: jcwalker3 / prgs-author (gitea-author)
WHO_IS_NEXT: a fresh prgs-author session to claim and implement #980 through the ordinary issue-author workflow, using the stable registry CAS and preserving active or ambiguously identified workers fail-closed.Issue claim heartbeat
[THREAD STATE LEDGER]
Canonical Issue State
STATE: implementation complete — PR #982 is open at exact head
c0c6d14b73add36ac157d501c603fd9ea51de1da, awaiting independent reviewWHO_IS_NEXT: reviewer
NEXT_ACTION: prgs-reviewer independently reviews PR #982 at head
c0c6d14b73add36ac157d501c603fd9ea51de1daagainst the corrected R3-C contract in this issue body.NEXT_PROMPT:
WHAT_HAPPENED: Implemented #980 through the ordinary author workflow at master
108cbfa173de34470d7abc022b929e6b3adc418a. Newmcp_fleet_retirement.registry_fingerprintderives the compare-and-swap token exclusively from canonical retirement-relevant registry content, with nosnapshot_at, wall-clock, request, report-generation, or serialization time, and with serialized rows sorted so iteration order cannot perturb it; a secondcandidate_fingerprintpins the exact candidate set the plan returned. The existing time-seededfleet_revision/registry_revisionis left exactly as it is — a separate token was the smaller and safer change, and the #978 snapshot shape and semantics are unchanged.WorkerRegistry.retire_stale_workersperforms the whole decision inside oneBEGIN IMMEDIATEtransaction: re-read rows, recompute the fingerprint from those rows, recompute the eligibility plan from those rows while re-reading active workflow leases, compare the candidate fingerprint, revalidate every target, then retire each survivor with a guardedUPDATEasserting its status, heartbeat, generation, session, fencing epoch, and pid are unchanged. Drift retires zero workers and reportsregistry_revision_movedorcandidate_set_moved; any exception rolls back and reportstransaction_failed, so a partial write is never reported as success. Retirement requires the full conjunction — active status, complete registry fields, parsable heartbeat, not live,pid_alive is False, expired heartbeat,ownership_state == "stale", canonical repository binding, no identity evidence shared with a live or unprobeable worker, and no active workflow-lease ownership — and everything else is preserved with a structured reason code. Retired rows become historical rather than stale and keep their history; nothing is deleted. Retirement does not repair untrusted live identity: live workers on legacy instance identities are preserved and keep theirlegacy_incomplete_identityfindings, so the result never claims the fleet became safe. The capability is exposed asgitea_plan_stale_worker_retirement(read-only) andgitea_apply_stale_worker_retirement, both restricted to controller/reconciler role kinds; the Gitea operation gate staysgitea.readbecause the mutation lands in the local control-plane registry, matching the #601 lease-lifecycle model, so no new Gitea write permission was introduced and no author permission was broadened.What is true now
status:pr-openafter the create-PR transition.feat/issue-980-stale-worker-retirement, worktreebranches/issue-980-stale-worker-retirement, base SHA108cbfa173de34470d7abc022b929e6b3adc418a, single commitc0c6d14b73add36ac157d501c603fd9ea51de1da, worktree clean after commit.master, headfeat/issue-980-stale-worker-retirement,mergeable: true,Closes #980present, 8 changed files, exactly one commit above master, remote head equal to local head.What changed
status:in-progressthenstatus:pr-open; one heartbeat comment; branchfeat/issue-980-stale-worker-retirementnon-force pushed; PR #982 created; this comment.mcp_fleet_retirement.py(new),mcp_worker_identity.py,gitea_mcp_server.py,task_capability_map.py,docs/stale-worker-retirement.md(new),docs/instance-fleet-identity.md,docs/mcp-tool-inventory.md,tests/test_issue_980_stale_worker_retirement.py(new).What is blocked
Who/what acts next
c0c6d14b73add36ac157d501c603fd9ea51de1da, with particular attention to token stability, transaction boundaries, race handling, and fail-closed preservation of active or ambiguously identified workers.Do not do: merge PR #982, self-review it, record a verdict from the author namespace, activate the BAA or any recovery grant, retire any real worker-registry row, or touch #981, PR #906, #650, review
624, branchfeat/issue-650-providers-insights, or worktreebranches/conflict-fix-pr-906-983e8ac.WHY: #980 carried the R3-C corrected stable-CAS contract with no external dependency, so the ordinary author cycle implemented that contract; an independent reviewer is required because an author may not review or merge their own PR.
RELATED_PRS: PR #982 implements this issue. PR #979 (#978) is the prerequisite that landed on master and supplies the fleet snapshot. PR #971 (#969) is an adjacent open PR that retires control-plane session rows in a different registry and shares three files as ordinary textual conflict risk, not a dependency. PR #906 (#650) is untouched.
BLOCKERS: None for the author phase; blocker classification: no blocker.
VALIDATION: prgs-author on the
gitea-authornamespace; identityjcwalker3, profileprgs-author, roleauthor, remoteprgs, repositoryScaled-Tech-Consulting/Gitea-Tools. Startup, runtime, local and live remote head all108cbfa173de34470d7abc022b929e6b3adc418a;in_parity:true,live_stale:false, parity-layermutation_safe:true; control checkout clean onmasterincluding untracked files. New suitetests/test_issue_980_stale_worker_retirement.py: 40 passed, 23 subtests passed, including the regression test proving the oldsnapshot_atderivation moved the token one second apart while the new registry CAS token does not. Adjacent suites (#980, #978, #975, #948, task-capability role invariants): 192 passed, 124 subtests passed. Full suite from the branch worktree: 28 failed, 6206 passed, 6 skipped, 1129 subtests passed, against a master baseline worktree at108cbfa173deof 28 failed, 6166 passed, 6 skipped, 1106 subtests passed — the failing set is identical, so zero regressions were introduced and no pre-existing failure was masked.LAST_UPDATED_BY: jcwalker3 / prgs-author (gitea-author)
WHO_IS_NEXT: prgs-reviewer to independently review PR #982 at exact head c0c6d14b73add36ac157d501c603fd9ea51de1da[THREAD STATE LEDGER]
Canonical Issue State
STATE: review 657 remediated — PR #982 advanced to exact head
e344e68a1345cb21db06776eef839f863b81f3e5, awaiting a fresh independent reviewWHO_IS_NEXT: reviewer
NEXT_ACTION: prgs-reviewer independently reviews PR #982 at exact head
e344e68a1345cb21db06776eef839f863b81f3e5and decides a new formal verdict; review 657 is stale and approves nothing.NEXT_PROMPT:
WHAT_HAPPENED: Addressed only the three blocking findings of formal review 657 (REQUEST_CHANGES by sysadmin at head
c0c6d14b73add36ac157d501c603fd9ea51de1da) and nothing else. One new commite344e68a1345cb21db06776eef839f863b81f3e5on branchfeat/issue-980-stale-worker-retirement, seven files, non-force fast-forward pushc0c6d14..e344e68.B1 — dedicated mutation authorization. Apply was gated by
_profile_operation_gate("gitea.read")and its immediate re-check resolved the same observational permission, so plan and apply shared one permission class and any profile that could look could also destroy. Apply now requires a dedicated capability,gitea.worker_registry.retire, declared once asRETIREMENT_MUTATION_PERMISSIONingitea_mcp_server.pyand mapped intask_capability_map.pyfor bothapply_stale_worker_retirementandgitea_apply_stale_worker_retirement. It is enforced at tool entry and re-resolved throughtask_capability_map.required_permissionimmediately before the registry mutation, so the two cannot drift. Plan keepsgitea.readand remains observational. The authorized role stays controller/reconciler, now as defence in depth behind the capability rather than the sole authority: an author, reviewer, or merger is refused even if mistakenly granted the permission. No profile holds the new permission by default, so author, reviewer, merger, and ordinary read-only profiles fail closed on the permission itself. No new Gitea write permission is introduced, no author permission is broadened, andretired_byand the acting identity still come from server-resolved context, never from caller input.B2 — affirmative identity and liveness proof. "Incomplete identity" previously meant only that pre-existing columns were non-null, which a
legacy-pid-…row satisfies trivially, so a row that proved nothing about which process it described was retireable. Retirement now requires a positive two-part proof inassess_retirement_identity_proof. Attribution: theclient_instance_idmust be launcher-mintedinst-…, decided through the existingmcp_fleet_snapshot.assess_instance_identityrather than a re-implemented rule. Fencing: newhost_id,boot_id, andprocess_start_timecolumns, added additively and probed by the newmcp_process_fencingmodule, which turn a bare pid into a statement about one process incarnation — which machine, which boot, which occupant of that pid number. Every probe returns None rather than guessing, and None always preserves. Rows written before these columns existed, and any row on a legacy instance identity, are preserved permanently and are retired only after their worker re-registers under a trusted identity. That legacy rows would otherwise remain outstanding indefinitely is explicitly not treated as grounds for a weaker proof, per the review's instruction. Also preserved fail-closed with structured reasons: unproven or mismatched host, unknown boot identity on either side, pid reuse (reported distinctly from a live worker), a second active row claiming the same instance and namespace while one may be live, conflicting session or generation evidence, live or unresolved lease ownership, and any active worker. A live pid remains an absolute block even across a boot boundary. Multiple processes of one legitimate cohort — one instance across distinct namespaces — remain distinguished from independent workers, which is why the conflict key is the (instance, namespace) pair rather than the instance alone.B3 — external-state fencing and transaction safety.
BEGIN IMMEDIATElocks the worker registry and nothing else, and the per-target loop runs after revalidation, so a lease acquired or a pid revived in that window went unnoticed — the registry-column guard could not catch it because no registry column changed. Two mechanisms now close it, both applied per target immediately before its own write.external_fence_fnis a version token over active leases fromexternal_state_fingerprint, captured inside the transaction before the authoritative read and re-compared before every guarded UPDATE; movement raises and rolls the whole transaction back, because a changed world invalidates every remaining decision, and an unreadable lease store raises rather than silently comparing equal.liveness_fnre-probes process liveness and fencing identity and must affirmatively re-establish that this exact process is gone, comparingprocess_start_timeso a pid reused since the plan is refused. A caller supplying no re-probe retires nothing rather than proceeding unfenced. The guarded UPDATE additionally assertshost_id,boot_id, andprocess_start_timeare unchanged, and all three participate in the CAS token, so fencing movement alone aborts. A rollback reportsexternal_state_movedortransaction_failedwithsuccess: false,retired_count: 0, andmutation_performed: false.What is true now
c0c6d14b73add36ac157d501c603fd9ea51de1daand now readsstale: true,review_feedback_stale: true,author_pushed_after_request_changes: true;approval_visible: falseandapproval_at_current_head: false. No verdict exists at the new head, and no merge operation was performed by this session.branches/issue-980-stale-worker-retirement, clean after commit including untracked files.master, head branchfeat/issue-980-stale-worker-retirement,mergeable: true, authorjcwalker3,Closes #980present, live head equal to the pushed commite344e68a1345cb21db06776eef839f863b81f3e5.gitea.readand that trustedinst-…provenance is not required. An author cannot edit a PR description through the sanctioned tools (gitea_edit_prhas noworktree_pathparameter, the #618 wall), so this comment is the authoritative account of the new head.What changed
feat/issue-980-stale-worker-retirementnon-force pushedc0c6d14..e344e68; this comment. No label change, no PR creation, no review verdict, no merge operation.mcp_process_fencing.py(new),mcp_fleet_retirement.py,mcp_worker_identity.py,gitea_mcp_server.py,task_capability_map.py,docs/stale-worker-retirement.md,tests/test_issue_980_stale_worker_retirement.py.registry_fingerprintstill excludes observation time, is order-stable and numeric-typing stable; plan still performs no mutation; registry or candidate movement still retires zero; active and uncertain workers remain fail-closed; retired rows remain historical and idempotent; ordinary author operations are not fleet-gated.Tests
tests/test_issue_980_stale_worker_retirement.py: 87 passed, 46 subtests passed, up from 40 passed / 23 subtests at the reviewed head. New classes cover each blocker's required regressions —DedicatedMutationCapabilityTests(read-only insufficient, author/reviewer/merger denied, granted-but-wrong-role denied, revoking the capability fails closed, granted controller reaches the isolated apply path on a temp database, plan stays read-only),AffirmativeIdentityProofTests(trusted stale row eligible, missing instance identity, legacy row without trusted provenance, each missing fencing column, pid reuse, same pid on another host, unprovable local host, unknown boot, prior boot, conflicting session, conflicting client instance, conflicting generation, live lease, unknown lease state, active worker, one cohort of five, independent workers, mixed eligible and uncertain, structured reason on every uncertain row), andExternalStateFencingTestsplusExternalStateFingerprintTests(lease acquired between plan and apply, fence moving mid-loop, unreadable lease store, worker becoming active, pid reuse before the write, host or boot movement before the write, omitted re-probe, heartbeat movement, generation and fencing movement, fencing columns in the CAS token, concurrent applies, later-candidate failure and rollback, database failure, counts matching committed state, fence-before-read ordering).e344e68: 28 failed, 6253 passed, 6 skipped, 1152 subtests passed.branches/baseline-980-108cbfadetached at108cbfa173de34470d7abc022b929e6b3adc418a, clean: 28 failed, 6166 passed, 6 skipped, 1106 subtests passed.FAILEDlines were captured from each worktree and compared withcomm; both directions are empty, so zero regressions were introduced and zero pre-existing failures were masked. The +87 passing delta is this branch's tests. Nothing was deselected; 6 skips are identical on both sides; no test timed out, and no flaky or environment-limited result was observed across the runs. The 28 pre-existing failures are unchanged and out of scope.What is blocked
gitea.worker_registry.retire, so apply is inert until an operator adds it to theallowed_operationsof the controller or reconciler profile inprofiles.json. That is a configuration change, and configuration was not modified by this work.Who/what acts next
e344e68a1345cb21db06776eef839f863b81f3e5, re-proving dedicated mutation authorization, affirmative identity and liveness safety, external-state fencing, transaction atomicity, and the regression tests before deciding a new formal verdict.Do not do: merge PR #982, self-review it, record a verdict from the author namespace, treat review 657 as approving the new head, grant
gitea.worker_registry.retireto any profile as part of this review, activate the BAA or any recovery grant, retire any real worker-registry row, or touch #981, PR #906, #650, review624, branchfeat/issue-650-providers-insights, or worktreebranches/conflict-fix-pr-906-983e8ac.WHY: Review 657 recorded three hard-rule blockers at the prior head — apply authorized solely by
gitea.read, complete legacy rows retireable without affirmative identity proof, and lease plus OS-liveness evidence unfenced at write time. Each is corrected at the new head with the required regression coverage, and because the head advanced the prior verdict is stale by construction, so an independent reviewer must decide afresh.RELATED_PRS: PR #982 implements this issue and now sits at head
e344e68a1345cb21db06776eef839f863b81f3e5. PR #979 (#978) is the prerequisite that already landed on master at108cbfa173de34470d7abc022b929e6b3adc418aand supplies the fleet snapshot this work builds on. PR #971 (#969) is an adjacent open PR retiring control-plane session rows in a different registry; it remains a textual conflict risk, not a dependency, and its start-time approach informed the fencing design here. PR #906 (#650) is untouched.BLOCKERS: None for the author phase; blocker classification: no blocker.
VALIDATION: prgs-author on the
gitea-authornamespace; identityjcwalker3verified bygitea_whoamiwithidentity_match: true, profileprgs-author, roleauthor, remoteprgs, repositoryScaled-Tech-Consulting/Gitea-Tools. Startup, runtime, local and live remote master head all108cbfa173de34470d7abc022b929e6b3adc418a;in_parity: true,stale: false,live_stale: false, parity-layermutation_safe: true. Control checkout clean onmasterincluding untracked files and equal to live master; all implementation, testing, commit, and push happened in the bound author worktree, never in the control checkout. Capabilities resolved immediately before each mutation in whoami-then-resolve order:push_branchandcomment_issue, bothallowed_in_current_session: truewithstop_required: falseandmutation_performed: false. No live worker-registry row was retired: every retirement test constructs a throwaway SQLite database throughtempfile.mkstemp, no apply ran against the live registry, and the new capability is unheld by every profile so it could not have run. Issue #981, PR #906, issue #650, review624, the BAA and recovery-grant proposals, unrelated branches and worktrees, profiles, configuration, credentials, sessions, and running MCP processes were not modified. Labels on #980 were left exactly as found.LAST_UPDATED_BY: jcwalker3 / prgs-author (gitea-author)
WHO_IS_NEXT: prgs-reviewer to independently review the new exact PR #982 head e344e68a1345cb21db06776eef839f863b81f3e5