Held callback wait reservation lifecycle

Follow-up to 855dfa3b8 on task/iw-wait-reservation. Object and message waits now use an actual endpoint reservation protocol across the host driver, Worker, local endpoint and ThreadManager. Existing frame guards, failure propagation and exact-link identity fixes remain. Independent-chain broker admission is unchanged. This branch remains held for review and real-VM tests.

Ownership and delivery

The shared ThreadSendWaits endpoint helper assigns each observed callback wait a monotonic epoch paired with its frame token. Another wait reached through owned resume/run gets another epoch, even if its handle/register values match. Missing and stale epochs cannot complete a new wait.

ThreadManager obtains an acknowledged reservation before calling the real, destructive object-wait resolver. Only the top owning frame can create that reservation. After the asynchronous acknowledgement it rechecks exact receiver liveness. Consumption and recording its result are synchronous on the host; the retained result is installed before awaiting completion transport. Concurrent resolver calls share one in-flight operation. Lost reservation replies consume nothing; lost completion replies retry the same retained value, never the synchronization resolver.

The endpoint retains the payload and delivery phase on the owning frame. Nested frames delay delivery until that exact frame is top again. Identical duplicates acknowledge without another stack pop; conflicting payloads fail. If nesting occurs after delivery but before resume, the driver preserves its blocked response and waits for its own frame. Message readiness/resume is one owned endpoint operation with the same epoch and duplicate acknowledgement.

Owned resume itself carries the consumed wait epoch. Its continuation response is retained separately until acknowledged, so losing a resume reply cannot run a newer wait or pop another frame. The link retries an ambiguous resume once with the same identity; a subsequent retry can recover the retained response. Repeated transport failure remains explicit, and the unacknowledged receipt prevents cleanup from aborting the newer wait. Acknowledgement, progress through a later epoch, or actual receiver retirement releases receipts. Lost housekeeping acks retain the receipt and do not invalidate an already-received continuation.

Cancellation, failure, fences and retirement

An unsatisfied object poll cancels its unconsumed reservation. Idle waits can still be aborted. Reserved, consumed/delivered, and unacknowledged-resume waits refuse live-frame abort explicitly; they cannot discard a consumed signal by restoring the caller. Host cleanup reports refusal instead of ignoring it. The owning resolver can cancel before consumption; it cannot cancel a retained consumed result as though untouched.

Stop/TerminateThread before consumption prevents consumption. Actual receiver retirement after consumption clears retained records and abandons mutexes by the exact receiving thread identity. Already-consumed auto-reset signals and semaphore units are not fabricated back into shared state. An exception with uncertain resolver side effects marks the transaction faulted: no retry of consumption, false cancellation, or silent abort; explicit owner retirement is required. Tests inject failures before and after real consumption.

Ordinary Worker slices were already fenced by callback ownership. This change also fences local main slices/main-yield resolution, unowned exports and other mutating Worker commands while callback waits are live. It uses an explicit audited register/global read set, not getter naming: get_sleep_yielded clears state, get_free_list flushes heap bins, get_edit_text writes guest memory, and has_pending_message pumps sockets. Those are blocked via unowned callExport and readExports; message progress calls its readiness function through the owned endpoint operation. Normal noncallback APIs keep their prior behavior. Expected command errors target their sequence rather than rejecting every unrelated request. The new helper is in both versioned browser graphs.

Source-only verification

node test/test-thread-send-wait-reservation.js
node test/test-thread-send-wait-identity.js
node tools/repro-thread-send-wait-ownership.js
node tools/repro-thread-send-wait-retirement.js
node test/test-thread-send-frame-ownership.js
node test/test-process-send-broker.js
node test/test-worker-thread-scheduler.js
node test/test-browser-cooperative-budget.js
node tools/check-browser-cache-versions.js
node tools/test-tiers.js --check

All pass. The original six nesting/epoch/message assertions and two abort-after-consumption assertions now pass through owned commands. The second fixture wait is entered through actual resume/run, not an unowned setter. The new suite covers lost reservation/completion/message/resume replies, duplicate/concurrent delivery, conflicting payloads, actual driver nesting between delivery and resume, cancellation, faults, and retirement. A separate real Node Worker uses actual ThreadManager.waitSingle against the same shared semaphore table and consumes only the remaining unit while the first result stays queued. Only handle metadata is mirrored, not table state or results. The fixture includes the actual destructive get_sleep_yielded body to verify that denied getter-shaped calls do not clear state and ordinary calls still do.

Three disposable loader mutations target the actual helper in both endpoints: removing top-frame deferral fails the nesting probe (resume versus pending); removing duplicate acknowledgement fails lost-reply retry (ESP 93 to 105); allowing reserved abort fails cancellation. Each negative exits 1; unmodified tests pass again. All fixture code is committed source, so no ignored build or external artifact is needed to reproduce it.

Remaining holds

This is not an independent-cycle scheduler or complete process/thread-generation protocol. Numeric-ID reuse across broader broker/cooperative lifetimes still needs that proof. Generic loader/network/render/I/O callback yields are not migrated to scoped exports here: unowned mutation while an outer wait is live fails explicitly. Those nested scenarios require migration before admission can be broadened. Full-VM cross-thread-send custom resolver calls are migrated but syntax-checked only. No full VM, x86 wait execution, build, browser, game, Boat or download ran. Browser cache checks verify source wiring, not runtime.