LearnNewsExamplesServices
Frontmatter
id17414
titleA lease-caused yield cannot release the lease, because the sweep never learns which cause stopped it
stateClosed
labels
bugaiagent-os
assigneesneo-gpt
createdAtAug 20, 2026, 1:55 PM
updatedAtAug 21, 2026, 1:22 PM
githubUrlhttps://github.com/neomjs/neo/issues/17414
authorneo-opus-vega
commentsCount2
parentIssue17380
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[x] 17398 The container tenant sweep never consults the lease it holds
blocking[]
closedAtAug 21, 2026, 1:22 PM

A lease-caused yield cannot release the lease, because the sweep never learns which cause stopped it

Closed Backlog/active-chunk-17 bugaiagent-os
neo-opus-vega
neo-opus-vega commented on Aug 20, 2026, 1:55 PM

Context

Split out of #17398 after @neo-gpt-emmy's Drop+Supersede of PR #17399 (review). Her corrected prescription names two obligations, and they are not one PR: own both outer-acquisition paths (#17398, amended) and a lease-specific sweep-exit contract (this leaf).

The starvation being fixed is live and dated. On an external tenant deployment, 2026-08-20: five maintenance tasks deferred since Aug 18–19 under holder tenant-repo-sync — graphlog-compaction, memory-summary-backfill, message-concept-harvest, provider-residency-repair, summary — with the orchestrator logging "the fairness yield bound has been exceeded; the lease pipeline is not admitting its waiters."

The Problem

Two yield causes require different exits, and the vote that reports them collapses to a boolean.

yield cause required exit
slice budget expired rotate to the next repo; the lease is still ours, keep it
outer lease outlived its hold bound stop admitting the tail after the active cohort, commit resumable state, release the outer lease

The refuted implementation composed voters with voters.some(voter => voter() === true). That is a shape error rather than a detail: all three lease-exit steps are conditional on the cause the OR discards. A sweep that learns only "someone said stop" cannot decide whether to keep rotating or to release, so it keeps rotating — which is precisely the observed behaviour, a holder that never admits its waiters.

Falsifier coordinates from the review: per-repo vote TenantRepoSyncService.mjs:2401-2417; all-repo admission TenantRepoSyncService.mjs:2922-2923; watchdog owner projection pipeline.mjs:786-787.

The Architectural Reality

The vote and the exit are separately owned. #17398 owns producing a cause-bearing verdict from both outer acquisitions; this leaf owns consuming it. The dependency is one-directional and strict: until a yield carries its cause, no exit branch here is expressible, so #17398 lands first.

ADR 0022 governs the mechanism: cooperative yield only. Releasing the outer lease on a lease-caused yield is the cooperative act — it is the holder standing down, not a preemption of it. #17379 died proposing hard preemption and that is not revisited here.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
sweep admission, TenantRepoSyncService.mjs:2922-2923 the yield verdict's cause (from #17398) a lease cause stops admitting further repos after the active cohort no cause / slice cause ⇒ rotation continues, unchanged cloud-deployment docs 5 waiters starved past the bound under one holder
active-cohort checkpoint the per-repo resumable state already written on partial progress commit attempted before a lease-yield outcome is returned, so the next acquisition resumes rather than re-materializes commit failure ⇒ fail loud, claim no successful checkpoint; the wrapper still releases — durable Chroma ids mean landed vectors are not re-embedded next sweep
outer lease release MaintenanceBackpressureService.mjs:1032-1068 — result.then(releaseLease, releaseLease) the wrapper is the sole release owner and releases on both settle paths; the sweep's only lever is ending the run early a cause the sweep cannot attribute ⇒ keep sweeping and report loudly — the holder currently never ends early, so the wrapper never gets to release
slice-cause rotation sliceBudgetMs unchanged, and asserted as an independent control — — the lease exit must not absorb slice behaviour

Decision Record impact

aligned-with ADR 0022 — cooperative yield only; no hard preemption, no leaseMonitor.mjs wiring.

Source corrections 2026-08-21 — two, both mine, both caught at intake

Raised by @neo-gpt during ticket-intake and verified independently at the coordinates he named on origin/dev@57061e9e7f before amending.

  1. A missing AC this ticket promised in prose and never wrote down. VectorService.mjs votes at the top of the embed loop, then await this.timeout(batchDelay), then slices the next stride — with no recheck. The bound can be exceeded during the wait and a full batch still gets bought. Now AC-5.

  2. This ticket prescribed the opposite of the real contract, and that is the serious one. Four ACs and two ledger rows treated "release the outer lease" as something the sweep does, including a commit-failure path where the sweep would hold it. The sweep cannot hold or release anything: MaintenanceBackpressureService.mjs:1032-1068 registers result.then(releaseLease, releaseLease), so the outer wrapper releases on both settle paths and is the sole release owner. The sweep's only lever is ending the run early. VectorService.mjs:1361 already says so in a comment — "The caller releases the lease on the yielded signal" — and I wrote the ticket against my model instead of that line.

Same failure shape as two other tickets of mine today: I described a consumer's behaviour from the wrong authority. Recorded here rather than silently edited, so the next reader sees the boundary that was wrong.

Acceptance Criteria

  • RED-PROOF, lease cause: a yield carrying a lease cause stops tail admission after the active cohort, commits that cohort's resumable state, and returns a lease-yield outcome so runTask ends early. Asserted on the production admission path, with the outer wrapper's release observed as the consequence of that early return — not on a stub, and not by the sweep calling release itself.
  • SILENT ARM, slice cause: a yield carrying a slice cause rotates to the next repo and does not end the run early, so the wrapper does not release. Without this arm, an implementation that returns early on every yield passes the arm above while destroying slice behaviour.
  • Ordering is a contract, not an accident. The active-cohort state commit is attempted before a lease-yield outcome is returned. Asserted by a commit failure: the run fails loud and must not claim a successful resumable checkpoint. It does not — and cannot — hold the lease: the outer wrapper releases on both settle paths, so a commit failure that reports honestly is the whole contract here. Durable Chroma ids mean a failed revisions-manifest write does not re-embed landed vectors next sweep, so there is no lost progress to hold a lease over.
  • An unattributable cause does not end the run early. A yield whose cause cannot be tied to an acquisition fails loud and keeps sweeping rather than returning a lease-yield outcome — a breaker that stands the holder down on ambiguity is worse than one that continues and reports.
  • The vote is re-consulted after batchDelay, before the next complete batch is bought. VectorService.mjs votes at the top of the loop, then await this.timeout(batchDelay), then slices the next stride with no recheck — so a lease can exceed its bound during the wait and still buy a full batch. A mutation deleting that second vote must turn exactly one arm red. Reviewer-verified on origin/dev@57061e9e7f.
  • Every production propagation edge is mutation-guarded. Deleting any one of the three lease-exit steps turns exactly one arm red. Stub-only dispatch evidence does not close this: the prior implementation's tests traversed a stub rather than the production edges, and that is why a green suite shipped a shape that could not release.
  • Plane-observable outcome: the watchdog's starved-waiter list drains under holder tenant-repo-sync rather than growing. Recorded as a post-merge observation with its own query, not as a merge gate — the merge gate is the arms above.

Out of Scope

  • Producing the cause. #17398 owns both outer-acquisition paths and the cause-bearing verdict; this leaf consumes it and lands second.
  • Hard preemption of a live holder — ADR 0022 anti-anchors it and #17379 died proposing it.
  • The slice budget's own value or phase accounting. Separate concern, and the epic #17411 lane covers what consumes the budget.

Avoided Traps

  • Do not re-introduce a boolean vote. A verdict that answers only true/false makes every requirement on this ticket unexpressible. That was the refuted PR's central error, and it was written as the elegant part.
  • Do not release the lease before committing state. A released lease over uncommitted progress re-buys the corpus on the next acquisition, which on a slow lane is the difference between a corpus that grows and one that burns compute at a constant count.
  • Do not assume one outer holder. The prior ticket named the CLI; MaintenanceBackpressureService.mjs:992-998 is a second outer acquisition, indistinguishable from it at the vote. That premise error is corrected on #17398 and must not be re-imported here.
  • Do not prove the exit with a stub. A stub records that the code intended to release. The starvation is evidence that intent is not the failing step.

Related

Parent: #17380. Depends on: #17398 (lands first). Superseded implementation: PR #17399. Governs: ADR 0022.

tobiu referenced in commit 2e8cbb8 - "feat(ai): the yield verdict reports which bound fired, not that one did (#17398) on Aug 21, 2026, 2:31 AM
tobiu referenced in commit d377507 - "test(ai): the consumption-absent boundary crosses production instead of a fixture (#17398) on Aug 21, 2026, 2:31 AM
tobiu referenced in commit a49655b - "feat(ai): both outer lease holders vote, and the vote reaches the sweep (#17398) (#17424) on Aug 21, 2026, 10:03 AM
tobiu referenced in commit 4384adb - "feat(agent-os): honor lease-caused tenant sync yields (#17414) (#17456) on Aug 21, 2026, 1:22 PM
tobiu closed this issue on Aug 21, 2026, 1:22 PM