diff --git a/src/shared/runtime-rpc-call-queue.test.ts b/src/shared/runtime-rpc-call-queue.test.ts index adf4e2399eb..1191d14c1e6 100644 --- a/src/shared/runtime-rpc-call-queue.test.ts +++ b/src/shared/runtime-rpc-call-queue.test.ts @@ -37,4 +37,16 @@ describe('runtime RPC call queue', () => { pending.shift()?.('background-2') await expect(background2).resolves.toBe('background-2') }) + + it('frees the queue slot when a runtime call throws synchronously', async () => { + const queue = new RuntimeRpcCallQueuePool(1, 1) + const first = queue.enqueue('web-runtime', 'status.get', () => { + throw new Error('invalid stored runtime pairing') + }) + + await expect(first).rejects.toThrow('invalid stored runtime pairing') + + const second = queue.enqueue('web-runtime', 'status.get', async () => 'second') + await expect(second).resolves.toBe('second') + }) }) diff --git a/src/shared/runtime-rpc-call-queue.ts b/src/shared/runtime-rpc-call-queue.ts index 7156685e80d..c42784e3f54 100644 --- a/src/shared/runtime-rpc-call-queue.ts +++ b/src/shared/runtime-rpc-call-queue.ts @@ -77,8 +77,10 @@ export class RuntimeRpcCallQueuePool { } // Why: runtime streams and worktree actions share transport capacity with // per-card status refreshes, so decorative calls must not stampede it. - void call - .run() + // Invoke through the promise chain so sync validation errors free the + // queue slot just like async transport failures. + void Promise.resolve() + .then(call.run) .then(call.resolve, call.reject) .finally(() => { queue.active = Math.max(0, queue.active - 1)