Keep completed OpenCode turns idle (#2336)
Ignore post-turn metadata updates for user messages already emitted so completed OpenCode turns remain idle.
This commit is contained in:
@@ -2728,6 +2728,65 @@ describe("OpenCode provider subagent contract", () => {
|
|||||||
await parent.close();
|
await parent.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("does not start a new autonomous turn when post-turn user message updates arrive for an already emitted message", async () => {
|
||||||
|
const { parent, openCode } = await createParentSession("ses_parent_post_turn");
|
||||||
|
openCode.sessionPromptAsyncEvents = [
|
||||||
|
...userMessageEvents({
|
||||||
|
sessionId: "ses_parent_post_turn",
|
||||||
|
messageId: "msg_user_1",
|
||||||
|
text: "Hello OpenCode",
|
||||||
|
}),
|
||||||
|
...assistantTurnEvents({
|
||||||
|
sessionId: "ses_parent_post_turn",
|
||||||
|
text: "Response from OpenCode",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
const events: AgentStreamEvent[] = [];
|
||||||
|
const streamDrained = createTestDeferred<void>();
|
||||||
|
parent.subscribe((event) => {
|
||||||
|
events.push(event);
|
||||||
|
if (event.type === "provider_subagent") {
|
||||||
|
streamDrained.resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await parent.startTurn("Hello OpenCode");
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(events).toContainEqual(expect.objectContaining({ type: "turn_completed" }));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Post-turn message update for the same user message ID that already completed
|
||||||
|
openCode.emitEvent({
|
||||||
|
type: "message.updated",
|
||||||
|
properties: {
|
||||||
|
info: { id: "msg_user_1", sessionID: "ses_parent_post_turn", role: "user" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
openCode.emitEvent({
|
||||||
|
type: "session.created",
|
||||||
|
properties: {
|
||||||
|
info: {
|
||||||
|
id: "ses_child_drain_marker",
|
||||||
|
parentID: "ses_parent_post_turn",
|
||||||
|
title: "Stream drain marker",
|
||||||
|
directory: "/workspace/repo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await streamDrained.promise;
|
||||||
|
|
||||||
|
// Verify that no new turn_started was emitted after turn_completed
|
||||||
|
const turnStartedCount = events.filter((e) => e.type === "turn_started").length;
|
||||||
|
expect(turnStartedCount).toBe(1);
|
||||||
|
} finally {
|
||||||
|
await parent.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
test("does not adopt late output from an interrupted Paseo turn", async () => {
|
test("does not adopt late output from an interrupted Paseo turn", async () => {
|
||||||
const { parent, openCode } = await createParentSession("ses_parent_interrupted");
|
const { parent, openCode } = await createParentSession("ses_parent_interrupted");
|
||||||
openCode.sessionPromptAsyncEvents = [];
|
openCode.sessionPromptAsyncEvents = [];
|
||||||
|
|||||||
@@ -3662,6 +3662,9 @@ class OpenCodeAgentSession implements AgentSession {
|
|||||||
// busy. That message is the earliest unambiguous boundary for a plugin-
|
// busy. That message is the earliest unambiguous boundary for a plugin-
|
||||||
// initiated parent turn; session metadata and assistant echoes are not.
|
// initiated parent turn; session metadata and assistant echoes are not.
|
||||||
if (event.type === "message.updated" && event.properties.info.role === "user") {
|
if (event.type === "message.updated" && event.properties.info.role === "user") {
|
||||||
|
if (this.emittedUserMessageIds.has(event.properties.info.id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!this.externallyDriven) {
|
if (!this.externallyDriven) {
|
||||||
|
|||||||
Reference in New Issue
Block a user