Skip to content

Commit 12d0fcf

Browse files
FUDCoclaude
andcommitted
fix: Make handleAck fire-and-forget to avoid RPC deadlock
In the browser runtime, when the kernel worker calls sendRemoteMessage, the offscreen document awaits sendWithAck which waits for an ACK. When the ACK arrives via remoteDeliver, the kernel worker calls handleAck back to the offscreen. If handleAck awaited, this creates a deadlock because the offscreen's RPC message handler is blocked on the original sendRemoteMessage request. Making handleAck fire-and-forget breaks the deadlock while still ensuring ACKs are processed correctly. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent a731f91 commit 12d0fcf

5 files changed

Lines changed: 21 additions & 11 deletions

File tree

packages/kernel-browser-runtime/src/PlatformServicesClient.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,13 +273,20 @@ export class PlatformServicesClient implements PlatformServices {
273273

274274
/**
275275
* Handle an acknowledgment from a peer for sent messages.
276+
* This is fire-and-forget to avoid deadlock: when the offscreen is awaiting
277+
* sendWithAck (waiting for ACK), the kernel worker may receive the ACK via
278+
* remoteDeliver and call handleAck back. If handleAck awaited, it would
279+
* deadlock because the offscreen can't process new RPC requests while
280+
* blocked on sendWithAck.
276281
*
277282
* @param peerId - The peer ID.
278283
* @param ackSeq - The sequence number being acknowledged.
279-
* @returns A promise that resolves when the acknowledgment has been processed.
280284
*/
281-
async handleAck(peerId: string, ackSeq: number): Promise<void> {
282-
await this.#rpcClient.call('handleAck', { peerId, ackSeq });
285+
handleAck(peerId: string, ackSeq: number): void {
286+
// Fire-and-forget RPC call to avoid deadlock
287+
this.#rpcClient.call('handleAck', { peerId, ackSeq }).catch((error) => {
288+
this.#logger.error('Error handling ACK:', error);
289+
});
283290
}
284291

285292
/**

packages/nodejs/src/kernel/PlatformServices.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -336,16 +336,19 @@ export class NodejsPlatformServices implements PlatformServices {
336336

337337
/**
338338
* Handle an acknowledgment from a peer for sent messages.
339+
* Fire-and-forget to match browser runtime semantics.
339340
*
340341
* @param peerId - The peer ID.
341342
* @param ackSeq - The sequence number being acknowledged.
342-
* @returns A promise that resolves when the acknowledgment has been processed.
343343
*/
344-
async handleAck(peerId: string, ackSeq: number): Promise<void> {
344+
handleAck(peerId: string, ackSeq: number): void {
345345
if (!this.#handleAckFunc) {
346346
throw Error('remote comms not initialized');
347347
}
348-
await this.#handleAckFunc(peerId, ackSeq);
348+
// Fire-and-forget - don't await
349+
this.#handleAckFunc(peerId, ackSeq).catch((error) => {
350+
this.#logger.error('Error handling ACK:', error);
351+
});
349352
}
350353

351354
/**

packages/ocap-kernel/src/remotes/RemoteHandle.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -447,9 +447,9 @@ export class RemoteHandle implements EndpointHandle {
447447
// Track received sequence number for piggyback ACK
448448
this.#remoteComms.updateReceivedSeq(this.#peerId, seq);
449449

450-
// Handle piggyback ACK if present
450+
// Handle piggyback ACK if present (fire-and-forget to avoid deadlock in browser runtime)
451451
if (ack !== undefined) {
452-
await this.#remoteComms.handleAck(this.#peerId, ack);
452+
this.#remoteComms.handleAck(this.#peerId, ack);
453453
}
454454

455455
let result = '';

packages/ocap-kernel/src/remotes/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export type StopRemoteComms = () => Promise<void>;
2424
export type RemoteComms = {
2525
getPeerId: () => string;
2626
sendRemoteMessage: SendRemoteMessage;
27-
handleAck: (peerId: string, ackSeq: number) => Promise<void>;
27+
handleAck: (peerId: string, ackSeq: number) => void;
2828
updateReceivedSeq: (peerId: string, seq: number) => void;
2929
issueOcapURL: (kref: string) => Promise<string>;
3030
redeemLocalOcapURL: (ocapURL: string) => Promise<string>;

packages/ocap-kernel/src/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,12 +367,12 @@ export type PlatformServices = {
367367
/**
368368
* Handle acknowledgment of received messages.
369369
* Implements cumulative ACK - acknowledges all messages with sequence <= ackSeq.
370+
* Fire-and-forget in browser runtime to avoid deadlock.
370371
*
371372
* @param peerId - The peer ID that sent the acknowledgment.
372373
* @param ackSeq - The highest sequence number being acknowledged.
373-
* @returns A promise that resolves when the acknowledgment has been processed.
374374
*/
375-
handleAck: (peerId: string, ackSeq: number) => Promise<void>;
375+
handleAck: (peerId: string, ackSeq: number) => void;
376376
/**
377377
* Update the highest received sequence number for a peer.
378378
* Used for tracking received messages to generate piggyback ACKs.

0 commit comments

Comments
 (0)