Skip to content

Commit fa1dfbd

Browse files
committed
🤖 fix: batch multiple queued messages with proper display
- Show /compact rawCommand only for single compaction request - Show actual message texts when multiple messages are queued (since compaction metadata is lost when options are overwritten) - Display now matches what will actually be sent - Multiple queued messages still sent together in single turn _Generated with `mux`_
1 parent cdeffed commit fa1dfbd

File tree

2 files changed

+77
-10
lines changed

2 files changed

+77
-10
lines changed

src/node/services/messageQueue.test.ts

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ describe("MessageQueue", () => {
3535
expect(queue.getDisplayText()).toBe("/compact -t 3000");
3636
});
3737

38-
it("should return rawCommand even with multiple messages if last has compaction metadata", () => {
38+
it("should show actual messages when compaction is added after normal message", () => {
3939
queue.add("First message");
4040

4141
const metadata: MuxFrontendMetadata = {
@@ -51,8 +51,9 @@ describe("MessageQueue", () => {
5151

5252
queue.add("Summarize this conversation...", options);
5353

54-
// Should use rawCommand from latest options
55-
expect(queue.getDisplayText()).toBe("/compact");
54+
// When multiple messages are queued, compaction metadata is lost when sent,
55+
// so display shows actual messages (not rawCommand) to match what will be sent
56+
expect(queue.getDisplayText()).toBe("First message\nSummarize this conversation...");
5657
});
5758

5859
it("should return joined messages when metadata type is not compaction-request", () => {
@@ -116,6 +117,68 @@ describe("MessageQueue", () => {
116117
});
117118
});
118119

120+
describe("multi-message batching", () => {
121+
it("should batch multiple follow-up messages", () => {
122+
queue.add("First message");
123+
queue.add("Second message");
124+
queue.add("Third message");
125+
126+
expect(queue.getMessages()).toEqual(["First message", "Second message", "Third message"]);
127+
expect(queue.getDisplayText()).toBe("First message\nSecond message\nThird message");
128+
});
129+
130+
it("should batch follow-up message after compaction", () => {
131+
const metadata: MuxFrontendMetadata = {
132+
type: "compaction-request",
133+
rawCommand: "/compact",
134+
parsed: {},
135+
};
136+
137+
queue.add("Summarize...", {
138+
model: "claude-3-5-sonnet-20241022",
139+
muxMetadata: metadata,
140+
});
141+
queue.add("And then do this follow-up task");
142+
143+
// When a follow-up is added, compaction metadata is lost (latestOptions overwritten),
144+
// so display shows actual messages to match what will be sent
145+
expect(queue.getDisplayText()).toBe("Summarize...\nAnd then do this follow-up task");
146+
// Raw messages have the actual prompt
147+
expect(queue.getMessages()).toEqual(["Summarize...", "And then do this follow-up task"]);
148+
});
149+
150+
it("should produce combined message for API call", () => {
151+
queue.add("First message", { model: "gpt-4" });
152+
queue.add("Second message");
153+
154+
const { message, options } = queue.produceMessage();
155+
156+
// Messages are joined with newlines
157+
expect(message).toBe("First message\nSecond message");
158+
// Latest options are used
159+
expect(options?.model).toBe("gpt-4");
160+
});
161+
162+
it("should batch messages with mixed images", () => {
163+
const image1 = { url: "data:image/png;base64,abc", mediaType: "image/png" };
164+
const image2 = { url: "data:image/jpeg;base64,def", mediaType: "image/jpeg" };
165+
166+
queue.add("Message with image", { model: "gpt-4", imageParts: [image1] });
167+
queue.add("Follow-up without image");
168+
queue.add("Another with image", { model: "gpt-4", imageParts: [image2] });
169+
170+
expect(queue.getMessages()).toEqual([
171+
"Message with image",
172+
"Follow-up without image",
173+
"Another with image",
174+
]);
175+
expect(queue.getImageParts()).toEqual([image1, image2]);
176+
expect(queue.getDisplayText()).toBe(
177+
"Message with image\nFollow-up without image\nAnother with image"
178+
);
179+
});
180+
});
181+
119182
describe("getImageParts", () => {
120183
it("should return accumulated images from multiple messages", () => {
121184
const image1 = {

src/node/services/messageQueue.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ function isCompactionMetadata(meta: unknown): meta is CompactionMetadata {
1919
* - Message texts (accumulated)
2020
* - Latest options (model, thinking level, etc. - overwrites on each add)
2121
* - Image parts (accumulated across all messages)
22+
*
23+
* Display logic:
24+
* - Single compaction request → shows rawCommand (/compact)
25+
* - Multiple messages → shows all actual message texts (since compaction metadata is lost anyway)
2226
*/
2327
export class MessageQueue {
2428
private messages: string[] = [];
@@ -63,17 +67,17 @@ export class MessageQueue {
6367

6468
/**
6569
* Get display text for queued messages.
66-
* Returns rawCommand if this is a compaction request, otherwise joined messages.
67-
* Matches StreamingMessageAggregator behavior.
70+
* - Single compaction request shows rawCommand (/compact)
71+
* - Multiple messages or non-compaction show actual message texts
6872
*/
6973
getDisplayText(): string {
70-
// Check if we have compaction metadata (cast from z.any() schema type)
71-
const cmuxMetadata = this.latestOptions?.muxMetadata as unknown;
72-
if (isCompactionMetadata(cmuxMetadata)) {
73-
return cmuxMetadata.rawCommand;
74+
// Only show rawCommand for single compaction request
75+
// (compaction metadata is only preserved when no follow-up messages are added)
76+
const muxMetadata = this.latestOptions?.muxMetadata as unknown;
77+
if (this.messages.length === 1 && isCompactionMetadata(muxMetadata)) {
78+
return muxMetadata.rawCommand;
7479
}
7580

76-
// Otherwise return joined messages
7781
return this.messages.join("\n");
7882
}
7983

0 commit comments

Comments
 (0)