Skip to content

Commit 058d93b

Browse files
committed
🤖 fix: prevent queued message amnesia by flushing partial before tool-call-end
When a queued message is sent after tool-call-end, the model needs to see the complete assistant response including tool results. Previously, there was a race condition where tool-call-end was emitted before the partial file was written, causing commitToHistory to read stale data. The fix ensures partial.json is flushed to disk BEFORE emitting the tool-call-end event, so listeners (like sendQueuedMessages) see the complete tool result when they read via commitToHistory. Changes: - Make completeToolCall async - Await flushPartialWrite before emitting tool-call-end - Update callers to await the async method _Generated with `mux`_
1 parent a2a417e commit 058d93b

File tree

1 file changed

+16
-11
lines changed

1 file changed

+16
-11
lines changed

‎src/node/services/streamManager.ts‎

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -566,9 +566,11 @@ export class StreamManager extends EventEmitter {
566566
}
567567

568568
/**
569-
* Complete a tool call by updating its part and emitting tool-call-end event
569+
* Complete a tool call by updating its part and emitting tool-call-end event.
570+
* CRITICAL: Flushes partial to disk BEFORE emitting event to prevent race conditions
571+
* where listeners (e.g., sendQueuedMessages) read stale partial data.
570572
*/
571-
private completeToolCall(
573+
private async completeToolCall(
572574
workspaceId: WorkspaceId,
573575
streamInfo: WorkspaceStreamInfo,
574576
toolCalls: Map<
@@ -578,7 +580,7 @@ export class StreamManager extends EventEmitter {
578580
toolCallId: string,
579581
toolName: string,
580582
output: unknown
581-
): void {
583+
): Promise<void> {
582584
// Find and update the existing tool part
583585
const existingPartIndex = streamInfo.parts.findIndex(
584586
(p) => p.type === "dynamic-tool" && p.toolCallId === toolCallId
@@ -609,7 +611,13 @@ export class StreamManager extends EventEmitter {
609611
}
610612
}
611613

612-
// Emit tool-call-end event
614+
// CRITICAL: Flush partial to disk BEFORE emitting event
615+
// This ensures listeners (like sendQueuedMessages) see the tool result when they
616+
// read partial.json via commitToHistory. Without this await, there's a race condition
617+
// where the partial is read before the tool result is written, causing "amnesia".
618+
await this.flushPartialWrite(workspaceId, streamInfo);
619+
620+
// Emit tool-call-end event (listeners can now safely read partial)
613621
this.emit("tool-call-end", {
614622
type: "tool-call-end",
615623
workspaceId: workspaceId as string,
@@ -618,9 +626,6 @@ export class StreamManager extends EventEmitter {
618626
toolName,
619627
result: output,
620628
} as ToolCallEndEvent);
621-
622-
// Schedule partial write
623-
void this.schedulePartialWrite(workspaceId, streamInfo);
624629
}
625630

626631
/**
@@ -762,8 +767,8 @@ export class StreamManager extends EventEmitter {
762767
const strippedOutput = stripEncryptedContent(part.output);
763768
toolCall.output = strippedOutput;
764769

765-
// Use shared completion logic
766-
this.completeToolCall(
770+
// Use shared completion logic (await to ensure partial is flushed before event)
771+
await this.completeToolCall(
767772
workspaceId,
768773
streamInfo,
769774
toolCalls,
@@ -799,8 +804,8 @@ export class StreamManager extends EventEmitter {
799804
: JSON.stringify(toolErrorPart.error),
800805
};
801806

802-
// Use shared completion logic
803-
this.completeToolCall(
807+
// Use shared completion logic (await to ensure partial is flushed before event)
808+
await this.completeToolCall(
804809
workspaceId,
805810
streamInfo,
806811
toolCalls,

0 commit comments

Comments
 (0)