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:
Matt Cowger
2026-07-23 10:01:14 +02:00
committed by GitHub
parent d1f19a5cdd
commit 31c8dc3f05
2 changed files with 62 additions and 0 deletions
@@ -2728,6 +2728,65 @@ describe("OpenCode provider subagent contract", () => {
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 () => {
const { parent, openCode } = await createParentSession("ses_parent_interrupted");
openCode.sessionPromptAsyncEvents = [];
@@ -3662,6 +3662,9 @@ class OpenCodeAgentSession implements AgentSession {
// busy. That message is the earliest unambiguous boundary for a plugin-
// initiated parent turn; session metadata and assistant echoes are not.
if (event.type === "message.updated" && event.properties.info.role === "user") {
if (this.emittedUserMessageIds.has(event.properties.info.id)) {
return false;
}
return true;
}
if (!this.externallyDriven) {