fix(claude): flush OutgoingMessageQueue before consuming next user turn (#909)

OutgoingMessageQueue.scheduleProcessing() defers socket.emit() via
setTimeout(fn,0) — a macrotask. The Claude SDK's nextMessage() callback
runs in a microtask chain, which executes before that macrotask fires.

This means messages-consumed for turn N+1 can be sent to the hub before
the queued agent messages from turn N have been emitted. The hub stamps
invokedAt on the N+1 user message at receive time, and then stores the
late-arriving agent messages with created_at > invokedAt_N+1. Since
compareMessages sorts by invokedAt ?? createdAt ascending, those agent
messages sort permanently below the N+1 user message.

Fix: await messageQueue.flush() at the top of nextMessage() so all
pending outgoing agent messages are sent through the socket before
messages-consumed is dispatched.

Closes #908

via [HAPI](https://hapi.run)

Co-authored-by: HAPI <noreply@hapi.run>
This commit is contained in:
SSU-WEI HUANG
2026-06-18 10:11:51 +08:00
committed by GitHub
co-authored by HAPI
parent a858256620
commit 4e4043d0e4
+8
View File
@@ -306,6 +306,14 @@ class ClaudeRemoteLauncher extends RemoteLauncherBase {
return permissionHandler.isAborted(toolCallId);
},
nextMessage: async () => {
// Flush any pending outgoing messages before consuming the next user
// turn. Without this, scheduleProcessing()'s setTimeout(fn,0) fires
// after the microtask that sends messages-consumed, causing the hub
// to stamp invokedAt on the next user message before it stores the
// current turn's queued agent messages — making them sort permanently
// below the next user message.
await messageQueue.flush();
if (pending) {
let p = pending;
pending = null;