Context
While building the last release-gating witness for #16551 (merged as PR #16619, 5ce07c4236), a test drove lease release under a stolen lifecycle guard for the first time in this repository. It did not fail on the property under test — it failed with:
ReferenceError: lifecycleGuardPath is not defined
at heavyMaintenanceLeasePrimitives.mjs:745
lifecycleGuardPath is called at four sites in ai/daemons/orchestrator/services/heavyMaintenanceLeasePrimitives.mjs and was never imported. Every guard-contention error path for lease release and renewal therefore threw a ReferenceError instead of its intended operator diagnostic — the message engineered to say which guard was stuck was itself the thing that crashed.
The one-line import is already fixed in PR #16619 and is not what this ticket is for. The finding is why a four-site defect survived: nothing had ever executed those paths.
The Problem
The four sites, all reachable only when enterLifecycleGuard / enterLifecycleGuardSync returns null (contention budget exhausted):
| line |
function |
variant |
:497 |
releaseHeavyMaintenanceLeaseSync |
sync |
:551 |
renewHeavyMaintenanceLeaseSync |
sync |
:746 |
releaseHeavyMaintenanceLease |
async |
:820 |
renewHeavyMaintenanceLease |
async |
Verified against test/playwright/unit/ai/daemons/orchestrator/services/HeavyMaintenanceLeaseService.spec.mjs (47 passing): the only guardContended assertion is at :1353, and it covers the acquire path. Release and renewal under contention have zero coverage in any variant.
This is the classic shape — the error path is the untested path — with a specific aggravating factor: these messages exist to be read by an operator during a live incident. A lease that cannot be released or renewed under contention is exactly the moment someone needs to know which guard path is stuck, and instead they got a stack trace with no path in it.
The blast radius is diagnostics, not correctness: the throw still happens, the caller still fails. But it fails uninformatively at the worst possible moment, and it did so undetected for as long as those lines have existed.
The Architectural Reality
ai/daemons/orchestrator/services/heavyMaintenanceLeasePrimitives.mjs — the four sites above. Imports its guard helpers from ai/daemons/shared/lifecycleGuard.mjs; lifecycleGuardPath is exported there (:58) and was simply absent from the import list.
ai/daemons/shared/lifecycleGuard.mjs — enterLifecycleGuard returns null after a bounded retry budget (100 attempts × 10ms). That null is the sole trigger for all four throws.
test/playwright/unit/ai/daemons/orchestrator/services/HeavyMaintenanceLeaseService.spec.mjs — the owning spec; :1353 covers acquire-under-contention and is the precedent to follow for shape.
The contention seam is already proven reachable from a test: PR #16619's a predecessor evicted inside manifest staging commits nothing over its successor holds a real guard via enterLifecycleGuard and drives production code into refusal. The same technique reaches all four sites.
The Fix
Add coverage for guard-contention refusal on release and renewal, in both sync and async variants, asserting on the message content rather than merely that it throws — a bare toThrow() would have passed against the ReferenceError and caught nothing.
Each test: hold a real lifecycle guard for the lease path, call the primitive, assert the thrown message names the lease's guard path.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Error Semantics |
Docs |
Evidence |
releaseHeavyMaintenanceLease contention throw (:746) |
existing |
unchanged — this ticket adds coverage only |
throws naming the guard path |
— |
a test holding a real guard asserts the path appears in the message |
renewHeavyMaintenanceLease contention throw (:820) |
existing |
unchanged |
same |
— |
same |
releaseHeavyMaintenanceLeaseSync contention throw (:497) |
existing |
unchanged |
same |
— |
same, sync variant |
renewHeavyMaintenanceLeaseSync contention throw (:551) |
existing |
unchanged |
same |
— |
same, sync variant |
No production surface changes. If writing the tests surfaces a further defect in these paths, that is a finding for a follow-up, not a scope expansion here.
Decision Record impact
none — test coverage for existing behavior; no boundary or contract moves.
Acceptance Criteria
Out of Scope
- The one-line import itself — already fixed in PR #16619. This ticket does not re-land it.
- Changing the throw semantics. Whether a contended release should throw rather than defer is a separate design question; this ticket covers what exists.
- A general audit of untested error paths across
ai/daemons/. Real and larger; this ticket is bounded to the four sites with a demonstrated defect.
- The atomic-write primitive consolidation in #16629. Adjacent file, different concern.
Avoided Traps
- Asserting only that it throws. The original defect did throw — a
ReferenceError. A bare toThrow() passes against exactly the bug this coverage exists to prevent. The assertion must read the message.
- Stubbing
enterLifecycleGuard to return null. That tests the branch, not the reachability; a refactor could make the branch unreachable in production while the stub keeps passing. Hold a real guard, as PR #16619's witness does.
- Assuming the sync variants mirror the async ones. They are four separate call sites; the missing import hit all four independently, which is precisely the evidence that a shared assumption about them is unsafe.
Related
#16551 / PR #16619 — where the defect surfaced and the import was repaired · #15763 — the commit-point fence work that owns this module's guard protocol · #16629 — adjacent ai/ primitive-consolidation work, different concern
Live latest-open sweep: checked latest 20 open issues at 2026-08-07T13:15Z; no equivalent found. A2A in-flight claim sweep over the 30 most recent messages (all read-states) at the same time; no overlapping claim.
Origin Session ID: cc25e2eb-2a9a-46dc-b068-3de4c792cd2e
Retrieval Hint: query_raw_memories("lifecycleGuardPath not imported ReferenceError lease release renewal guard contention error path untested")
Retrieval Hint: the discriminating probe is holding a real lifecycle guard via enterLifecycleGuard and then calling release/renew — the first test to do so is a predecessor evicted inside manifest staging commits nothing over its successor in TenantRepoSyncService.spec.mjs.
Context
While building the last release-gating witness for #16551 (merged as PR #16619,
5ce07c4236), a test drove lease release under a stolen lifecycle guard for the first time in this repository. It did not fail on the property under test — it failed with:lifecycleGuardPathis called at four sites inai/daemons/orchestrator/services/heavyMaintenanceLeasePrimitives.mjsand was never imported. Every guard-contention error path for lease release and renewal therefore threw aReferenceErrorinstead of its intended operator diagnostic — the message engineered to say which guard was stuck was itself the thing that crashed.The one-line import is already fixed in PR #16619 and is not what this ticket is for. The finding is why a four-site defect survived: nothing had ever executed those paths.
The Problem
The four sites, all reachable only when
enterLifecycleGuard/enterLifecycleGuardSyncreturnsnull(contention budget exhausted)::497releaseHeavyMaintenanceLeaseSync:551renewHeavyMaintenanceLeaseSync:746releaseHeavyMaintenanceLease:820renewHeavyMaintenanceLeaseVerified against
test/playwright/unit/ai/daemons/orchestrator/services/HeavyMaintenanceLeaseService.spec.mjs(47 passing): the onlyguardContendedassertion is at:1353, and it covers the acquire path. Release and renewal under contention have zero coverage in any variant.This is the classic shape — the error path is the untested path — with a specific aggravating factor: these messages exist to be read by an operator during a live incident. A lease that cannot be released or renewed under contention is exactly the moment someone needs to know which guard path is stuck, and instead they got a stack trace with no path in it.
The blast radius is diagnostics, not correctness: the throw still happens, the caller still fails. But it fails uninformatively at the worst possible moment, and it did so undetected for as long as those lines have existed.
The Architectural Reality
ai/daemons/orchestrator/services/heavyMaintenanceLeasePrimitives.mjs— the four sites above. Imports its guard helpers fromai/daemons/shared/lifecycleGuard.mjs;lifecycleGuardPathis exported there (:58) and was simply absent from the import list.ai/daemons/shared/lifecycleGuard.mjs—enterLifecycleGuardreturnsnullafter a bounded retry budget (100 attempts × 10ms). Thatnullis the sole trigger for all four throws.test/playwright/unit/ai/daemons/orchestrator/services/HeavyMaintenanceLeaseService.spec.mjs— the owning spec;:1353covers acquire-under-contention and is the precedent to follow for shape.The contention seam is already proven reachable from a test: PR #16619's
a predecessor evicted inside manifest staging commits nothing over its successorholds a real guard viaenterLifecycleGuardand drives production code into refusal. The same technique reaches all four sites.The Fix
Add coverage for guard-contention refusal on release and renewal, in both sync and async variants, asserting on the message content rather than merely that it throws — a bare
toThrow()would have passed against theReferenceErrorand caught nothing.Each test: hold a real lifecycle guard for the lease path, call the primitive, assert the thrown message names the lease's guard path.
Contract Ledger Matrix
releaseHeavyMaintenanceLeasecontention throw (:746)renewHeavyMaintenanceLeasecontention throw (:820)releaseHeavyMaintenanceLeaseSynccontention throw (:497)renewHeavyMaintenanceLeaseSynccontention throw (:551)No production surface changes. If writing the tests surfaces a further defect in these paths, that is a finding for a follow-up, not a scope expansion here.
Decision Record impact
none— test coverage for existing behavior; no boundary or contract moves.Acceptance Criteria
releaseHeavyMaintenanceLeaseunder a held guard throws a message containing the lease's guard path, asserted on content — not a baretoThrow().renewHeavyMaintenanceLeaseunder a held guard, same assertion.releaseHeavyMaintenanceLeaseSyncunder a held guard, same assertion.renewHeavyMaintenanceLeaseSyncunder a held guard, same assertion.lifecycleGuardPathimport — a test that passes against the originalReferenceErroris not coverage. Reverting the one-line import locally is the control.finally, so a failing assertion cannot strand the guard for sibling specs in the same worker.HeavyMaintenanceLeaseService.spec.mjsremains green in full.Out of Scope
ai/daemons/. Real and larger; this ticket is bounded to the four sites with a demonstrated defect.Avoided Traps
ReferenceError. A baretoThrow()passes against exactly the bug this coverage exists to prevent. The assertion must read the message.enterLifecycleGuardto returnnull. That tests the branch, not the reachability; a refactor could make the branch unreachable in production while the stub keeps passing. Hold a real guard, as PR #16619's witness does.Related
#16551 / PR #16619 — where the defect surfaced and the import was repaired · #15763 — the commit-point fence work that owns this module's guard protocol · #16629 — adjacent
ai/primitive-consolidation work, different concernLive latest-open sweep: checked latest 20 open issues at 2026-08-07T13:15Z; no equivalent found. A2A in-flight claim sweep over the 30 most recent messages (all read-states) at the same time; no overlapping claim.
Origin Session ID: cc25e2eb-2a9a-46dc-b068-3de4c792cd2e
Retrieval Hint:
query_raw_memories("lifecycleGuardPath not imported ReferenceError lease release renewal guard contention error path untested")Retrieval Hint: the discriminating probe is holding a real lifecycle guard via
enterLifecycleGuardand then calling release/renew — the first test to do so isa predecessor evicted inside manifest staging commits nothing over its successorinTenantRepoSyncService.spec.mjs.