From b3adaf8d828037447d1c5a8e691fe555f2ac67ed Mon Sep 17 00:00:00 2001 From: miaojixuezhang Date: Sun, 2 Aug 2026 05:48:37 +0900 Subject: [PATCH] fix(opencode): finish prompt loop by parent link --- packages/opencode/src/session/prompt.ts | 2 +- packages/opencode/test/session/prompt.test.ts | 33 ++++++++++++++++--- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index eb116f6b960f..22b1d7d99a2a 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1112,7 +1112,7 @@ const layer = Layer.effect( lastAssistant?.finish && !["tool-calls"].includes(lastAssistant.finish) && !hasToolCalls && - lastUser.id < lastAssistant.id + lastAssistant.parentID === lastUser.id ) { const orphan = lastAssistantMsg?.parts.find( (part): part is SessionV1.ToolPart => part.type === "tool" && isOrphanedInterruptedTool(part), diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 491ad06aaf47..f7588936c115 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -368,10 +368,10 @@ const succeedVoid = (deferred: Deferred.Deferred) => { Effect.runSync(Deferred.succeed(deferred, void 0).pipe(Effect.ignore)) } -const user = Effect.fn("test.user")(function* (sessionID: SessionID, text: string) { +const user = Effect.fn("test.user")(function* (sessionID: SessionID, text: string, id = MessageID.ascending()) { const session = yield* Session.Service const msg = yield* session.updateMessage({ - id: MessageID.ascending(), + id, role: "user", sessionID, agent: "build", @@ -388,11 +388,14 @@ const user = Effect.fn("test.user")(function* (sessionID: SessionID, text: strin return msg }) -const seed = Effect.fn("test.seed")(function* (sessionID: SessionID, opts?: { finish?: string }) { +const seed = Effect.fn("test.seed")(function* ( + sessionID: SessionID, + opts?: { finish?: string; userID?: MessageID; assistantID?: MessageID }, +) { const session = yield* Session.Service - const msg = yield* user(sessionID, "hello") + const msg = yield* user(sessionID, "hello", opts?.userID) const assistant: SessionV1.Assistant = { - id: MessageID.ascending(), + id: opts?.assistantID ?? MessageID.ascending(), role: "assistant", parentID: msg.id, sessionID, @@ -460,6 +463,26 @@ noLLMServer.instance( { config: cfg }, ) +noLLMServer.instance( + "loop exits when a client-generated user id sorts after its server assistant id", + () => + Effect.gen(function* () { + const prompt = yield* SessionPrompt.Service + const sessions = yield* Session.Service + const chat = yield* sessions.create({ title: "Pinned" }) + const seeded = yield* seed(chat.id, { + finish: "stop", + userID: MessageID.make("msg_ffff_client_clock"), + assistantID: MessageID.make("msg_0000_server_clock"), + }) + + expect(seeded.assistant.id < seeded.user.id).toBe(true) + const result = yield* prompt.loop({ sessionID: chat.id }) + expect(result.info.id).toBe(seeded.assistant.id) + }), + { config: cfg }, +) + it.instance("loop exits without an LLM request for interrupted orphan tool calls", () => Effect.gen(function* () { const { llm } = yield* useServerConfig(providerCfg)