---
id: 11515
title: Add release-timing consumer-guidance to HeavyMaintenanceLeaseService JSDoc + reference-impl example
state: CLOSED
labels:
  - documentation
  - enhancement
  - ai
  - model-experience
assignees:
  - neo-opus-ada
createdAt: '2026-05-17T01:33:26Z'
updatedAt: '2026-05-17T06:15:43Z'
githubUrl: 'https://github.com/neomjs/neo/issues/11515'
author: neo-opus-ada
commentsCount: 0
parentIssue: null
subIssues: []
subIssuesCompleted: 0
subIssuesTotal: 0
blockedBy: []
blocking: []
closedAt: '2026-05-17T06:15:43Z'
---
# Add release-timing consumer-guidance to HeavyMaintenanceLeaseService JSDoc + reference-impl example

Authored by Claude Opus 4.7 (Claude Code). Session f662d055-a35b-446a-83ff-5fc859604722.

Friction-to-gold follow-up from PR #11509 review cycles 1 and 2 (per @neo-gpt MESSAGE:3c42e809-d1e9-43ca-a10c-9bdc9a08eb7d routing recommendation: *"file a tiny follow-up after A so the backup PR does not grow a sidecar surface"*).

## Context

PR #11509 (Lane C of #11503) shipped consumer-side wrapping of 4 manual heavy-maintenance CLI scripts in `withHeavyMaintenanceLease`. Two CHANGES_REQUESTED review cycles by @neo-gpt surfaced the SAME root failure mode at two different surfaces:

- **Cycle 1** (`PRR_kwDODSospM8AAAABAJIdTg`): the catch path on lease-acquisition failure fell through to `GraphService.decayGlobalTopology()` — graph mutation while the lease was NEVER acquired
- **Cycle 2** (`PRR_kwDODSospM8AAAABAJKF8w`): the success path placed `GraphService.decayGlobalTopology()` after `await withHeavyMaintenanceLease(...)` — graph mutation AFTER the helper's `finally` released the lease

Both fail-classes are substrate-mutation outside the lease window. Both shipped initially because the consumer-side mental model of the lease primitive is non-obvious: developers (and LLMs) naturally read `await wrapper(...); doX()` as "doX runs AFTER wrapper's effects", but JS await unblocks BEFORE the wrapper's own `finally` fires. The release-in-finally contract of `withHeavyMaintenanceLease` is therefore counter-intuitive at the call site.

## The Problem

The primitive's API surface (`ai/daemons/services/HeavyMaintenanceLeaseService.mjs`) currently documents WHAT the wrapper does but NOT the timing-semantics consumers need to honor for substrate-protection correctness. Specifically:

1. No JSDoc on `withHeavyMaintenanceLease` calls out that the `finally`-based release happens BEFORE the awaited promise settles
2. No reference-impl example shows the "graph-mutation inside an inner-finally" pattern that Lane C's `runSandman.mjs` arrived at via 2 review cycles
3. The risk class is invisible from the call site — a future consumer wrapping a new heavy script will likely make the same mistake without empirical hints

The MX cost is concrete: 2 full PR review cycles + 2 commits + author + reviewer + 3rd reviewer attention. Friction → gold candidate per AGENTS.md §13.2.

## Architectural Reality

- `ai/daemons/services/HeavyMaintenanceLeaseService.mjs:257-280` — `withHeavyMaintenanceLease(task, options)` definition + finally-based release
- `ai/daemons/services/HeavyMaintenanceLeaseService.mjs:163` — `acquireHeavyMaintenanceLease` core acquisition
- `buildScripts/ai/runSandman.mjs:176-258` — current reference-impl pattern (decay inside inner finally; landed via PR #11509 cycles 1+2)
- `test/playwright/unit/ai/daemons/services/HeavyMaintenanceLeaseService.spec.mjs` — current spec (8/8) verifies acquisition/release contract but does not pin the consumer-side anti-pattern
- `test/playwright/unit/ai/buildScripts/laneCManualScriptLeaseAdoption.spec.mjs:113-160` — release-timing invariant test on `runSandman.mjs` (pattern-pin via offset + structural finally-block regex)

## The Fix

Three coordinated edits:

1. **JSDoc augmentation on `withHeavyMaintenanceLease`** in `ai/daemons/services/HeavyMaintenanceLeaseService.mjs:257`. Add explicit timing-semantic callout above the `@param task` block:

   > ⚠️ **Release timing:** the lease is released in this helper's `finally` block. JS await semantics guarantee the helper's `finally` runs AFTER the awaited `task` settles but BEFORE this helper's returned promise resolves. Consequence: code placed AFTER `await withHeavyMaintenanceLease(...)` runs OUTSIDE the lease window. If your task has side effects that must remain inside the lease window (e.g., graph mutation, Chroma writes, SQLite writes), place them inside an inner `try { ... } finally { /* side-effect */ }` of the task itself — NOT after the await. See `buildScripts/ai/runSandman.mjs` for the canonical pattern.

2. **Reference-impl example** in the JSDoc — a runnable code block showing the inner-finally pattern, contrasting with the wrong shape:

   ```js
   // ❌ WRONG: graph mutation happens outside the lease (post-await = post-release)
   await withHeavyMaintenanceLease(async () => { /* heavy work */ }, {owner: 'X'});
   await mutateGraph();  // <-- lease already released by helper's finally

   // ✅ RIGHT: graph mutation inside inner finally runs while lease still held
   await withHeavyMaintenanceLease(async () => {
       try {
           /* heavy work */
       } finally {
           await mutateGraph();  // <-- runs before await settles, before helper's finally
       }
   }, {owner: 'X'});
   ```

3. **Spec assertion** in `HeavyMaintenanceLeaseService.spec.mjs` pinning the release-timing contract: a test that wraps a task with an inner finally calling a probe, asserts probe-call timestamp is BEFORE the release-probe timestamp (release-after-task ordering). This becomes the contract anchor for the JSDoc claim — JSDoc + spec must agree.

## Contract Ledger Matrix

| Target Surface | Source of Authority | Proposed Behavior | Fallback | Docs | Evidence |
|---|---|---|---|---|---|
| `withHeavyMaintenanceLease` JSDoc | `HeavyMaintenanceLeaseService.mjs:257` + this ticket | Explicit release-timing callout + ✓/✗ reference-impl example | N/A | The JSDoc IS the doc | Static read of JSDoc + visual confirmation in `ask_knowledge_base` results |
| `withHeavyMaintenanceLease` release-timing contract | `HeavyMaintenanceLeaseService.spec.mjs` + this ticket | Spec test pins "task's inner finally runs BEFORE wrapper's release" ordering | N/A | Spec name self-documents | Test fail-mode: any future refactor that releases before task settles fails |

## Acceptance Criteria

- [ ] **AC1**: `withHeavyMaintenanceLease` JSDoc in `HeavyMaintenanceLeaseService.mjs` includes an explicit release-timing callout naming `finally`-based release AND the post-await-runs-outside-lease consequence
- [ ] **AC2**: JSDoc includes a runnable code-block example contrasting WRONG (post-await mutation) vs RIGHT (inner-finally mutation) shapes, with the existing `runSandman.mjs` as a referenced canonical pattern
- [ ] **AC3**: New spec test in `HeavyMaintenanceLeaseService.spec.mjs` empirically pins the release-timing ordering: task's inner `finally` runs BEFORE wrapper's release call (assertion via probe-timestamps or call-order array)
- [ ] **AC4**: Existing 8/8 tests in `HeavyMaintenanceLeaseService.spec.mjs` continue to pass; new test brings the count to 9/9
- [ ] **AC5**: `ask_knowledge_base(query='how do I correctly use withHeavyMaintenanceLease for graph mutation')` returns a synthesized answer that includes the inner-finally pattern (verifies the JSDoc augmentation actually flows into the KB synthesis layer)

## Out of Scope

- **Re-entrancy / owner-inheritance semantics** (e.g., child process of an owner-X task inheriting the parent's lease) — that's a deeper architectural change with its own design tradeoffs; file separately if needed
- **Auto-detection of the wrong-shape pattern via static analysis / lint** — too speculative for v1; JSDoc + reference-impl + spec is sufficient
- **Refactoring `withHeavyMaintenanceLease` to return-before-release** — would break the release-on-task-completion guarantee; not the right substrate change

## Avoided Traps

- **Adding the guidance ONLY in JSDoc without a spec assertion**: rejected — JSDoc rots silently when a future refactor changes the release-timing behavior. Spec assertion is the load-bearing contract.
- **Adding the guidance ONLY in a separate guide / `learn/` doc**: rejected — consumers reading `HeavyMaintenanceLeaseService.mjs` won't see it. JSDoc lives at the consumption point.
- **Adding the guidance as a section in `AGENTS.md` / atlas**: rejected — wrong tier per substrate accretion defense (§13). Consumer-side guidance for a specific primitive belongs at the primitive's API surface, not in always-loaded substrate.

## Related

- **Source friction**: PR #11509 cycles 1 + 2 (review IDs `PRR_kwDODSospM8AAAABAJIdTg`, `PRR_kwDODSospM8AAAABAJKF8w`)
- **Substrate primitive**: PR #11506 / #11505 (HeavyMaintenanceLeaseService)
- **Consumer impl reference**: `buildScripts/ai/runSandman.mjs` post-PR #11509 cycles 1+2
- **Routing authority**: @neo-gpt MESSAGE:3c42e809-d1e9-43ca-a10c-9bdc9a08eb7d ("file a tiny follow-up after A so the backup PR does not grow a sidecar surface")
- **Umbrella context**: #11503 (broader heavy-maintenance mutex epic; this ticket is NOT a lane, just a consumer-guidance follow-up)

## Handoff Retrieval Hints

- Retrieval Hint: `withHeavyMaintenanceLease release timing finally consumer guidance`
- Retrieval Hint: PR #11509 cycles 1+2 (the empirical anchor cycle)
- Retrieval Hint: Commit SHAs `375a1e90c` (cycle-1 fix) + `3631fb361` (cycle-2 fix) — the consumer-side patterns this ticket documents

Origin Session ID: f662d055-a35b-446a-83ff-5fc859604722

## Timeline

- 2026-05-17T01:33:28Z @neo-opus-ada added the `documentation` label
- 2026-05-17T01:33:28Z @neo-opus-ada added the `enhancement` label
- 2026-05-17T01:33:28Z @neo-opus-ada added the `ai` label
- 2026-05-17T01:33:28Z @neo-opus-ada added the `model-experience` label
- 2026-05-17T01:37:10Z @neo-opus-ada assigned to @neo-opus-ada
- 2026-05-17T01:43:09Z @neo-opus-ada cross-referenced by PR #11518
- 2026-05-17T01:52:51Z @neo-opus-ada cross-referenced by #11519
- 2026-05-17T02:06:04Z @neo-opus-ada referenced in commit `a46509a` - "fix(ai): address PR #11518 cycle 1 — wrapper-return shape table + remove fragile line-self-reference (#11515)

Per @neo-gpt cycle 1 review (PRR_kwDODSospM8AAAABAJO13Q), two contract-text
corrections in the withHeavyMaintenanceLease JSDoc:

1. Returned-shape table previously listed `'stale'` as a wrapper return status,
   but the live code normalizes ALL acquired outcomes (acquired, acquired-after-stale,
   acquired-after-malformed) into `{status: 'completed', acquired: true, ...}` from
   the wrapper's perspective. Restructured into TWO tables:
   - Wrapper return shape: 2 rows (`completed`, `held`) + note on theoretical
     pass-through for non-acquired non-held edge cases
   - Acquisition descriptor passed to `task`: 3 rows enumerating internal
     recovery telemetry (acquired / acquired-after-stale / acquired-after-malformed)
     with previousStatus field semantics
   Closes the contract drift that would have poisoned KB ingestion if the table
   shipped as-was.

2. Removed `line ~272` self-reference from the JSDoc release-timing prose.
   Line numbers shift with nearby edits; replaced with stable "this helper's own
   `finally` block (the one that wraps the `task` invocation below and calls
   `releaseHeavyMaintenanceLease`)" relative pointer.

No runtime behavior change. JSDoc + structural-precision only.

Test evidence:
- npm run test-unit -- test/playwright/unit/ai/daemons/services/HeavyMaintenanceLeaseService.spec.mjs
  -> 9/9 passed in 635ms (unchanged from cycle 0)

(#11515)"
- 2026-05-17T02:18:53Z @neo-opus-ada cross-referenced by PR #11521
- 2026-05-17T06:15:43Z @tobiu closed this issue
- 2026-05-17T06:15:43Z @tobiu referenced in commit `5468a10` - "feat(ai): document withHeavyMaintenanceLease release-timing + reference-impl + spec (#11515) (#11518)

* feat(ai): document withHeavyMaintenanceLease release-timing semantics + reference-impl + spec test (#11515)

Friction-to-gold follow-up from PR #11509 cycles 1 + 2 (per @neo-gpt routing
recommendation MESSAGE:3c42e809: file a tiny follow-up rather than bundling
into Lane A's backup PR).

## What changed

1. `ai/daemons/services/HeavyMaintenanceLeaseService.mjs` — augmented JSDoc on
   `withHeavyMaintenanceLease`:
   - Explicit release-timing callout naming the finally-based release + the
     5-step await ordering trace
   - 'Right shape' code block (substrate mutation inside an inner finally
     of the wrapped task)
   - 'Wrong shape' code block (substrate mutation after the await — outside
     the lease)
   - Returned-shape table for completed / held / stale statuses
   - Cross-references to runSandman.mjs (canonical consumer), PR #11509
     (empirical anchor), and this ticket #11515
   - No code-semantics changes

2. `test/playwright/unit/ai/daemons/services/HeavyMaintenanceLeaseService.spec.mjs` —
   new test 'withLease release-timing invariant: task inner finally runs INSIDE
   the lease window (#11515)'. Captures lease-file presence at three probe
   points and asserts:
   - probe 1 (inside task body): lease ACTIVE
   - probe 2 (inside task's inner finally): lease STILL ACTIVE -- load-bearing
     assertion for the canonical inner-finally pattern
   - probe 3 (after await settles): lease MISSING
   Strict execution order asserted via call-order array.
   A future refactor that releases the lease before the task's inner finally
   fails probe 2.

## Test evidence

- npm run test-unit -- test/playwright/unit/ai/daemons/services/HeavyMaintenanceLeaseService.spec.mjs
  -> 9/9 passed in 627ms (8 baseline + new release-timing test)
- git diff --check origin/dev...HEAD -> clean (no trailing whitespace)
- Branched from origin/dev at a5c638069 (post-Lane-C merge); fresh branch

## Acceptance criteria coverage

- AC1: JSDoc names finally-based release + post-await-outside-lease consequence
- AC2: JSDoc includes runnable code-block example contrasting right vs wrong shapes
- AC3: Spec test pins task's inner finally runs BEFORE wrapper's release
- AC4: 8/8 existing tests still pass; total 9/9
- AC5: ask_knowledge_base verification deferred until KB re-indexes post-merge

## Coordination

- Lane-claim broadcast: MESSAGE:a1adf7d9 (non-colliding with Claude PR #11514
  Lane A, GPT #10117/#11516 DevIndex lanes, Gemini #11475 memory-core lane)
- Authority anchor: PR #11509 cycles 1+2 review IDs PRR_kwDODSospM8AAAABAJIdTg
  and PRR_kwDODSospM8AAAABAJKF8w

(#11515)

* fix(ai): address PR #11518 cycle 1 — wrapper-return shape table + remove fragile line-self-reference (#11515)

Per @neo-gpt cycle 1 review (PRR_kwDODSospM8AAAABAJO13Q), two contract-text
corrections in the withHeavyMaintenanceLease JSDoc:

1. Returned-shape table previously listed `'stale'` as a wrapper return status,
   but the live code normalizes ALL acquired outcomes (acquired, acquired-after-stale,
   acquired-after-malformed) into `{status: 'completed', acquired: true, ...}` from
   the wrapper's perspective. Restructured into TWO tables:
   - Wrapper return shape: 2 rows (`completed`, `held`) + note on theoretical
     pass-through for non-acquired non-held edge cases
   - Acquisition descriptor passed to `task`: 3 rows enumerating internal
     recovery telemetry (acquired / acquired-after-stale / acquired-after-malformed)
     with previousStatus field semantics
   Closes the contract drift that would have poisoned KB ingestion if the table
   shipped as-was.

2. Removed `line ~272` self-reference from the JSDoc release-timing prose.
   Line numbers shift with nearby edits; replaced with stable "this helper's own
   `finally` block (the one that wraps the `task` invocation below and calls
   `releaseHeavyMaintenanceLease`)" relative pointer.

No runtime behavior change. JSDoc + structural-precision only.

Test evidence:
- npm run test-unit -- test/playwright/unit/ai/daemons/services/HeavyMaintenanceLeaseService.spec.mjs
  -> 9/9 passed in 635ms (unchanged from cycle 0)

(#11515)

---------

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>"

