Win16 DDE incoming transaction ownership

2026-09-22: fixed two data-handle leaks in win16_dde_deliver on main.

The POKE/EXECUTE/ADVSTART branch used select(data_take(...), 0, is_poke). WebAssembly evaluates both operands: EXECUTE and ADVSTART allocated a handle and discarded it. Sixteen transactions exhausted the entire data pool. Separately, a POKE allocated data before attempting to enqueue its callback; a full four-entry callback queue rejected the callback without freeing data.

Allocation is now conditional on POKE. Ownership transfers only when the callback queue accepts it; rejection frees the newly allocated handle. The existing callback completion path continues to reclaim accepted data.

node test/test-win16-dde-data-ownership.js calls the real frame delivery and callback-completion helpers, with a seeded conversation. Before the fix it reported 16 leaked handles for each non-data transaction kind, 16 live handles for four queued POKEs, and 12 remaining after callback completion. After the fix all pass, including payload bytes and 64 further POKE/reclaim cycles.

Fragment balance, API hash table, handler ESP/epilogue checks, test-tier membership, whitespace, silent-stub pin (247 manual + 22 metadata), and duplicate census (131 groups / 509 members) pass.

The broader test-win16-dde-room.js and test-win16-dde-connect-callback.js both trap at connection setup, before this transaction branch. Repeating each with only this fragment replaced in memory by its pre-change HEAD contents reproduces the same unreachable stack (functions 8109, 8400, 776, 9863). Thus these suites are not passing in the shared worktree; the failure is independent of this patch. No shared files were reverted for the control runs. Investigating that connection-pump failure is a next candidate.

Follow-up: connection-pump trap resolved

Compiling the same source closure with its diagnostic name section identified the failing function as win16_enter_wndproc. The existing DBG_INV=1 trace showed a null procedure, window 15 and WM_SIZE immediately after the DDE callback returned. WIN16_DDE_CB and WIN16_CONT_CREATE_SIZE both occupied offset 0xFF80 in WIN16_THUNK_SEL; win16_dispatch handles the latter first. It consequently interpreted a DDE return as a CreateWindow stack continuation.

Moved the DDE callback to the unused offset 0xFF84. This changes only an emulator-private return address, not the guest DDE API. The new test/test-win16-thunk-offsets.js inspects the source manifest's constant Win16 globals in the reserved 0xFF00..0xFFFF range and rejects duplicate offsets. It fails on the old collision and passes with 25 unique entries. It is a unit regression, not a complete parser for arbitrary computed slot definitions or an allocator for future continuation slots.

After this fix the real Win16 task/loopback tests pass: room routing 18/18, connect callbacks and transactions 25/25. The earlier failure above is retained as the investigation history, not the current outcome. No browser or native Windows capture was needed to establish this internal address collision.

Follow-up: one enqueue ownership rule for POKE and ADVDATA

The ADVDATA branch had the same rejected-queue leak as POKE. Extending the ownership regression to frame type 10 reproduced 16 live handles for four callbacks and 12 remaining after completion. The shared ask_push_data now consumes its data handle on every call: accepted data remains owned by the callback, rejected data is freed immediately. The previous POKE-only cleanup was removed. All production callers were inspected: POKE and ADVDATA supply freshly allocated blocks; the wrapper, XACT_COMPLETE and ADVREQ supply zero.

The expanded regression passes both frame types through queue saturation, checks accepted payloads survive rejection, checks completion releases all handles, then alternates 64 POKE/ADVDATA delivery/completion cycles. The real Win16 callback/transaction suite remains 25/25 green. Fragment, test-tier and silent-stub gates pass. Other agents' service/connection changes in this file were not included in this fix. EXECUTE semantics, string ownership and malformed frames remain separate review work; this does not claim all DDE lifetime paths are now correct.

This is an internal ownership regression, not a native Win98 conformance capture or an end-to-end Hearts gameplay claim. EXECUTE's existing callback argument representation is unchanged and still needs separate review. Also open: malformed frame lengths, pool exhaustion behavior, string-handle ownership, other queue-producing branches, and eager-select trace reads.