lib/token-allowance-gateway.ts:246-264 — _acquireConcurrency only handles signal?.aborted at enqueue time:
this._concurrencySemaphore.queue.push(entry);
if (signal?.aborted) { /* splice + reject */ }
// no signal.addEventListener('abort', ...) for a later abort
If the signal aborts after the entry is queued (the common case — the user cancels while waiting), nothing removes entry. When a slot later frees, _releaseConcurrency calls entry() which resolves a promise no one is awaiting, and the paired releaseConcurrency() in approve's finally may never run (the caller already threw from the mutex/abort path), leaking a permit.
Impact
Concurrency permits leak under cancellation; after _maxConcurrency cancelled-while-queued operations, the gateway deadlocks — no approve can acquire a slot.
Suggested fix
Mirror the Mutex implementation: attach an abort listener that dequeues and rejects the entry, with cleanup.
lib/token-allowance-gateway.ts:246-264—_acquireConcurrencyonly handlessignal?.abortedat enqueue time:If the signal aborts after the entry is queued (the common case — the user cancels while waiting), nothing removes
entry. When a slot later frees,_releaseConcurrencycallsentry()which resolves a promise no one is awaiting, and the pairedreleaseConcurrency()inapprove'sfinallymay never run (the caller already threw from the mutex/abort path), leaking a permit.Impact
Concurrency permits leak under cancellation; after
_maxConcurrencycancelled-while-queued operations, the gateway deadlocks — noapprovecan acquire a slot.Suggested fix
Mirror the
Muteximplementation: attach anabortlistener that dequeues and rejects the entry, with cleanup.