Skip to content

Commit ec0f150

Browse files
committed
fix(cli): clear delivered image-only queue entries
The queue-removal branch in _handleAgentEvent was gated on the user message having text, so an image-only steer or follow-up — a pasted screenshot sent mid-turn — was never spliced out after delivery: pendingMessageCount stayed above zero and the entry lingered in the queue. The queues already store "" for such messages, so matching on the empty text like any other message is enough. Ported from upstream pi.
1 parent a5feced commit ec0f150

3 files changed

Lines changed: 85 additions & 12 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@knightcodeai/cli": patch
3+
---
4+
5+
Clear a delivered steering or follow-up message that carried only images. The
6+
entry previously stayed in the queue forever, leaving the pending count wrong
7+
and the message re-queued.

‎packages/cli/src/core/agent-session.ts‎

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -626,19 +626,17 @@ export class AgentSession {
626626
if (event.type === "message_start" && event.message.role === "user") {
627627
this._overflowRecoveryAttempted = false;
628628
const messageText = contentText(event.message.content, "");
629-
if (messageText) {
630-
// Check steering queue first
631-
const steeringIndex = this._steeringMessages.indexOf(messageText);
632-
if (steeringIndex !== -1) {
633-
this._steeringMessages.splice(steeringIndex, 1);
629+
// Check steering queue first
630+
const steeringIndex = this._steeringMessages.indexOf(messageText);
631+
if (steeringIndex !== -1) {
632+
this._steeringMessages.splice(steeringIndex, 1);
633+
this._emitQueueUpdate();
634+
} else {
635+
// Check follow-up queue
636+
const followUpIndex = this._followUpMessages.indexOf(messageText);
637+
if (followUpIndex !== -1) {
638+
this._followUpMessages.splice(followUpIndex, 1);
634639
this._emitQueueUpdate();
635-
} else {
636-
// Check follow-up queue
637-
const followUpIndex = this._followUpMessages.indexOf(messageText);
638-
if (followUpIndex !== -1) {
639-
this._followUpMessages.splice(followUpIndex, 1);
640-
this._emitQueueUpdate();
641-
}
642640
}
643641
}
644642
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import type { AgentTool } from "@knightcode/agent";
2+
import { fauxAssistantMessage, fauxToolCall } from "@knightcode/ai";
3+
import { Type } from "typebox";
4+
import { afterEach, describe, expect, it } from "vitest";
5+
import { createHarness, type Harness } from "../harness.ts";
6+
7+
describe("issue #8581 image-only queue entry", () => {
8+
const harnesses: Harness[] = [];
9+
10+
afterEach(() => {
11+
while (harnesses.length > 0) {
12+
harnesses.pop()?.cleanup();
13+
}
14+
});
15+
16+
it("removes an image-only steering message from the queue after delivery", async () => {
17+
let releaseToolExecution: (() => void) | undefined;
18+
const toolRelease = new Promise<void>((resolve) => {
19+
releaseToolExecution = resolve;
20+
});
21+
const waitTool: AgentTool = {
22+
name: "wait",
23+
label: "Wait",
24+
description: "Wait for release",
25+
parameters: Type.Object({}),
26+
execute: async () => {
27+
await toolRelease;
28+
return { content: [{ type: "text", text: "released" }], details: {} };
29+
},
30+
};
31+
32+
const harness = await createHarness({ tools: [waitTool] });
33+
harnesses.push(harness);
34+
35+
const waitForToolStart = new Promise<void>((resolve) => {
36+
const unsubscribe = harness.session.subscribe((event) => {
37+
if (event.type === "tool_execution_start" && event.toolName === "wait") {
38+
unsubscribe();
39+
resolve();
40+
}
41+
});
42+
});
43+
44+
let sawImage = false;
45+
harness.setResponses([
46+
fauxAssistantMessage(fauxToolCall("wait", {}), { stopReason: "toolUse" }),
47+
(context) => {
48+
const user = [...context.messages].reverse().find((message) => message.role === "user");
49+
sawImage =
50+
user?.role === "user" &&
51+
typeof user.content !== "string" &&
52+
user.content.some((part) => part.type === "image");
53+
return fauxAssistantMessage("done");
54+
},
55+
]);
56+
57+
const promptPromise = harness.session.prompt("start");
58+
await waitForToolStart;
59+
await harness.session.steer("", [{ type: "image", mimeType: "image/png", data: "ZmFrZQ==" }]);
60+
expect(harness.session.pendingMessageCount).toBe(1);
61+
releaseToolExecution?.();
62+
await promptPromise;
63+
64+
expect(sawImage).toBe(true);
65+
expect(harness.session.agent.hasQueuedMessages()).toBe(false);
66+
expect(harness.session.pendingMessageCount).toBe(0);
67+
});
68+
});

0 commit comments

Comments
 (0)