#2155·iii

Native channels: completed public-API channels retain entries after transfer and creator disconnect

Author: McHersheysCreated Sep 9, 2026Updated Sep 9, 2026

Summary

Channels created through the public engine::channels::create API retain their registry entry/access key after both endpoints have completed. They also remain after the creating worker disconnects. Reusing a completed reference still receives HTTP 101 and then a WebSocket close because the endpoint has already been taken.

This is not a claim that transferred data can be replayed. Wrong keys and unknown channel IDs were denied. The concern is bounded reclamation of terminal channel records and the absence of a supported caller-side cancellation/reclamation path for a long-lived channel gateway.

Versions and observed reproduction

  • Engine release: iii/v0.23.0, source 8356a0012a22e7d5732b49328215f9ebfbbc7bfd.
  • Node SDK and public browser SDK: 0.20.0.
  • Actual browser and actual registered native worker, not a mocked Engine.
  • Two channels, buffer size 2; one finite 1,048,576-byte binary transfer in each direction, in 8,192-byte application chunks.

Reproduction sequence (the equivalent sequence below describes the executed fixture; it is not a claim that a newly reduced standalone script was separately run):

  1. Register a creating worker and a distinct byte-processing worker.
  2. Call createChannel(creator, 2) from iii-sdk/helpers.
  3. Pass the complementary reference to a registered function. The Node SDK resolves the reference to ChannelReader/ChannelWriter before the handler runs.
  4. Connect the public browser ChannelWriter or ChannelReader to the other reference through a fixed native-channel-only proxy. Transfer the finite source, close the writer, consume EOF, and await the native function result. Verify length, SHA-256 and bytes.
  5. Try a new WebSocket handshake to the same native /ws/channels/{channel_id}?key={access_key}&dir={direction} reference.
  6. Shut down the creator. Independently poll engine::workers::list through the other worker until the original creator ID is absent. Repeat step 5.
  7. As negative controls, try a random key for the existing ID and the real key for a random ID.

Observed for both directions:

json
{
  "after_transfer": {"handshakeAccepted": true, "closed": true},
  "after_creator_disconnect": {"handshakeAccepted": true, "closed": true},
  "creatorAbsent": true,
  "wrongKeyDenied": true,
  "unknownChannelDenied": true
}

Both binary transfers passed. The fully completed reference remained addressable after independently confirmed creator absence. All fixture resources were then removed by exact ownership. This was a small bounded reproduction, not a production leak-rate measurement.

Current main source inspection

The relevant paths are still present at 741e2a6e42984ea9cde83d7757055944926c37ad (inspection only; the runtime reproduction above used the stated release):

  • engine::channels::create passes None as owner_worker_id, so remove_channels_by_worker cannot reclaim these public-API channels.
  • sweep_stale_channels intentionally excludes channels whose sender and receiver have both been taken. Its comment expects ws_handler or views.rs to clean them up.
  • handle_channel_socket closes/drops the endpoints but does not remove the registry record. The read side also waits on rx.recv() without separately observing a disconnected idle reader.
  • The existing stale-channel test explicitly retains the both-taken state, which cannot currently distinguish active from terminal.

I did not wait for the five-minute sweeper in the runtime fixture; its inability to reclaim the both-taken case is a source-level finding, not an additional timed runtime observation.

Expected behavior / smallest proposed repair

Please confirm whether there is already a supported cancellation/reclamation API or a newer supported remedy that this misses.

Otherwise, a narrowly scoped native lifecycle repair appears necessary:

  • Track actual endpoint completion/cancellation separately from whether an endpoint was taken. Reclaim a terminal channel and reject further use of its references.
  • Preserve a writer-completed channel while a legitimate reader still needs to connect/drain buffered data; simply removing the record on any socket close would be incorrect.
  • Observe disconnect/cancellation while an idle reader or backpressured writer is waiting. A failed duplicate attachment must not tear down the legitimate active transfer.
  • Bind public creation to the authenticated creator where the worker/session contract supports it, or provide an exact authorized cancellation operation; do not trust a caller-supplied arbitrary owner ID.
  • Add bounded regressions for normal drain, delayed reader, duplicate attachment, early reader disconnect, blocked writer, explicit cancellation and creator disconnect.

No Engine patch or engine-recycling workaround has been deployed. We are keeping bulk transfer separate from ordinary JSON RPC rather than increasing RPC envelope limits.