LearnNewsExamplesServices
Frontmatter
id13780
titleHeavy-lease cooperative-yield primitive: shouldYieldHeavyMaintenanceLease + maxActiveHoldMs knob
stateClosed
labels[]
assigneesneo-opus-grace
createdAtJun 21, 2026, 5:17 PM
updatedAtJun 21, 2026, 7:17 PM
githubUrlhttps://github.com/neomjs/neo/issues/13780
authorneo-opus-grace
commentsCount0
parentIssue13624
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJun 21, 2026, 7:17 PM

Heavy-lease cooperative-yield primitive: shouldYieldHeavyMaintenanceLease + maxActiveHoldMs knob

neo-opus-grace
neo-opus-grace commented on Jun 21, 2026, 5:17 PM

Context

Sub of #13624 (orchestrator heavy-maintenance fairness). Root-cause located: the long summary task holds the heavy-maintenance lease for its whole backlog run (SummaryService.mjs:221-247 loops through all pending sessions in one lease-hold via gemma-4-31b), so an overdue dream (and its dependent golden-path) gets its window hours late behind the summary. Calibrated by @neo-opus-vega + @tobiu: the dream is not never-starved — it runs, but LONG/late. This is a fairness/latency defect: no single heavy task should be able to hold the lease long enough to starve a timely peer.

Per the #13624 divide-and-conquer (@neo-opus-vega), this leaf is the lease-domain half: the cooperative-yield primitive + the maxActiveHoldMs knob. (Ada owns the SummaryService chunked-yield + the picker window; Vega owns the Decision Record / fairness contract.)

The Problem

withHeavyMaintenanceLease holds the lease for the entire duration of task(). A long task (summary of N sessions) therefore monopolizes the single heavy-maintenance lease — there is no mechanism for it to cooperatively yield mid-run so an overdue higher-need task (dream) can interleave. The mutex is correct; what's missing is a bounded-active-hold signal a long task can poll to release-and-requeue.

The Architectural Reality

  • ai/daemons/orchestrator/services/HeavyMaintenanceLeaseService.mjsacquireHeavyMaintenanceLease / withHeavyMaintenanceLease / releaseHeavyMaintenanceLease + the lease descriptor (acquiredAt, staleAfterMs). staleAfterMs (currently 6h) governs dead-holder reclamation — orthogonal to the active monopoly this leaf addresses.
  • The lease descriptor already carries acquiredAt, so active-hold age is computable read-only.

The Fix

A pure, exported helper, consumed by long heavy tasks:

  • shouldYieldHeavyMaintenanceLease({acquiredAt, now, maxActiveHoldMs}) — returns true when now - acquiredAt > maxActiveHoldMs. A long task polls it between work units and, when true, returns early (releasing the lease) so the picker can grant an overdue peer its window; the next periodic-sweep re-acquires for the remaining work.

The maxActiveHoldMs value/default and the caller wiring are OUT of scope for #13780 (see Evolution): the helper is value-agnostic — the caller passes the budget. The value belongs to #13624 (Vega's fairness Decision Record); the wiring to the consuming integration (Ada's SummaryService chunked-yield).

This is the lease-domain primitive the SummaryService chunked-yield (Ada) calls; it's uniform, so any heavy task inherits the fairness floor.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback / Edge Case Docs Evidence
shouldYieldHeavyMaintenanceLease (NEW exported helper) this ticket + #13624 pure: true when active-hold exceeds maxActiveHoldMs maxActiveHoldMs unset/0 ⇒ never yields (back-compat: current behavior) JSDoc unit: exceeds/within/boundary + unset
maxActiveHoldMs config leafDESCOPED from #13780 (see Evolution) #13624 (Vega) + the consuming integration the helper is value-agnostic; the caller supplies the budget — value + wiring are NOT #13780's scope Vega's ADR n/a here

Evolution

Contract reconciliation (per @neo-gpt's #13781 review, 2026-06-21): #13780 ships the value-agnostic helper only (shouldYieldHeavyMaintenanceLease). The maxActiveHoldMs value/default and the caller wiring are explicitly out of scope — the helper takes the budget as a parameter; the value belongs to #13624 (Vega's fairness Decision Record) and the wiring to the consuming integration (Ada's SummaryService chunked-yield). This removes the close-target mismatch: #13781 (the helper + its unit tests) fully satisfies #13780's reconciled contract. The keep-vs-close question for the helper itself (uniform time-floor vs per-task count-caps) is the separate open #13624 convergence.

Acceptance Criteria

  • shouldYieldHeavyMaintenanceLease({acquiredAt, now, maxActiveHoldMs}) returns true iff now - acquiredAt > maxActiveHoldMs; false when within, at boundary, or maxActiveHoldMs falsy (back-compat).
  • Pure + read-only — does not mutate the lease or touch the mutex/release path.
  • Unit coverage: exceeds / within / boundary / unset-knob.
  • JSDoc documents it as the cooperative-yield signal a long heavy task polls; distinct from staleAfterMs (dead-holder reclamation).

Out of Scope

  • The SummaryService chunked-yield integration (process M per hold, release-and-requeue) — Ada, #13624.
  • The picker giving the overdue dream its window — Ada, #13624.
  • The fairness Decision Record / ADR — Vega, #13624.
  • staleAfterMs re-tuning — separate (dead-holder latency), tracked under #13624 if pursued.

Related

  • Parent fairness epic: #13624
  • Root-cause sibling: #13750 (golden-path latency), #13755 (heavy-lease typed-outcome)

Live latest-open sweep: checked latest 25 open issues 2026-06-21; no equivalent lease-yield ticket found.

Origin Session ID: 80932414-00dc-4d2e-96e6-d0bcf1529733

Retrieval Hint: "heavy-maintenance lease cooperative-yield maxActiveHoldMs summary monopolizes dream"

tobiu referenced in commit 91afaeb - "feat(ai): heavy-lease cooperative-yield primitive shouldYieldHeavyMaintenanceLease (#13780) (#13781) on Jun 21, 2026, 7:17 PM
tobiu closed this issue on Jun 21, 2026, 7:17 PM