fix(events): give a lease's ledger row a reconcile path (Phase 11b)
A lease row had no reconcile path at all, and nothing failed to say so.
`cleanup.js` resolves a resource to the action of the step that made it, and for
a lease that action is `core.lease` -- a CORE action, on a path a module cannot
register anything on. So every `override` row came back `unanswered` for the life
of the run, and a lease the shard had quietly dropped (a config lease is
memory-only there, so a restart reverts it by design) stayed in the ledger as
live until teardown went hunting a baseline nobody was holding.
`core.lease` gains a `reconcile()`, and `registerEventLeases` gains an optional
`inForce()`: "does the game side still have any record of this hold?"
Deliberately not `read()` plus a comparison. A value that differs from what the
run applied is DRIFT, which teardown must deliver through `restore()` so the row
lands `drifted` with the current value beside it; a reconcile that inferred
absence from a changed value would orphan the row first and tell the operator the
lease vanished rather than that somebody moved it. Only an explicit
`{ ok: true, held: false }` takes a row out -- a throw, a timeout, an
unrecognised shape and a lease with no `inForce()` all leave the ledger alone.
MODULE_API_VERSION stays 1.10.0, amended in place.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -422,6 +422,58 @@ const ACTIONS = [
|
||||
await require('../events/ledger').markRunDirty(runId)
|
||||
return { ok: true }
|
||||
},
|
||||
|
||||
/**
|
||||
* Which of this run's leases the game side still has a record of (Phase 11b).
|
||||
*
|
||||
* **A lease row had no reconcile path at all until this existed**, and nothing
|
||||
* failed to say so. `cleanup.js` resolves a resource to the action of the step
|
||||
* that made it, and for a lease that action is `core.lease` — a CORE action, on
|
||||
* a path a module cannot register anything on. So every `override` row came
|
||||
* back `unanswered` for the life of the run, and a lease the shard had quietly
|
||||
* dropped (a restart reverts every config lease, by design) stayed in the
|
||||
* ledger as live until teardown went looking for a baseline nobody was holding.
|
||||
*
|
||||
* The question asked is deliberately NOT "is the value still what we applied".
|
||||
* That is drift, and drift is teardown's verdict to deliver through `restore`
|
||||
* so the row lands as `drifted` with the current value beside it. A reconcile
|
||||
* that inferred absence from a changed value would orphan the row first and
|
||||
* throw that away — the operator would be told the lease vanished rather than
|
||||
* that somebody moved it.
|
||||
*
|
||||
* A lease with no `inForce()` is reported in force, which is core's posture
|
||||
* everywhere else in this file: "I could not ask" must never be recorded as
|
||||
* "it is gone".
|
||||
*/
|
||||
async reconcile({ resources }) {
|
||||
const inForce = []
|
||||
|
||||
for (const row of resources || []) {
|
||||
if (row.kind !== 'override') continue
|
||||
|
||||
const lease = registries.eventLease(row.ref)
|
||||
|
||||
if (!lease || typeof lease.inForce !== 'function') {
|
||||
inForce.push(row.ref)
|
||||
continue
|
||||
}
|
||||
|
||||
let answer
|
||||
try {
|
||||
answer = await lease.inForce({ ref: row.ref, payload: row.payload || null })
|
||||
} catch (err) {
|
||||
answer = null
|
||||
}
|
||||
|
||||
// Only an explicit `held: false` takes a row out. A module that threw, timed
|
||||
// out, or answered something unrecognisable has not said the lease is gone.
|
||||
if (answer && answer.ok === true && answer.held === false) continue
|
||||
|
||||
inForce.push(row.ref)
|
||||
}
|
||||
|
||||
return { ok: true, inForce }
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user