diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 9a2cfd84c28..c144c17b252 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1231,6 +1231,10 @@ export const layer = Layer.effect( } if (input.noReply === true) return message + const result = yield* loop({ sessionID: input.sessionID }) + if (result.info.id > message.info.id) return result + // The prompt may have joined a loop that was already exiting after its + // last message snapshot. Run once more so the saved message is consumed. return yield* loop({ sessionID: input.sessionID }) }) diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 315a124ad36..4d7a6b0eba7 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -1226,6 +1226,62 @@ it.instance( 3_000, ) +it.instance( + "prompt submitted while an active run exits starts another loop pass", + () => + Effect.gen(function* () { + const { llm } = yield* useServerConfig(providerCfg) + const prompt = yield* SessionPrompt.Service + const run = yield* SessionRunState.Service + const sessions = yield* Session.Service + const chat = yield* sessions.create({ title: "Pinned" }) + yield* seed(chat.id, { finish: "stop" }) + const stale = (yield* sessions.messages({ sessionID: chat.id })).findLast((msg) => msg.info.role === "assistant") + if (!stale) throw new Error("expected seeded assistant") + const started = yield* Deferred.make() + const release = yield* Deferred.make() + + const active = yield* run + .ensureRunning( + chat.id, + Effect.succeed(stale), + Deferred.succeed(started, void 0).pipe(Effect.andThen(Deferred.await(release)), Effect.as(stale)), + ) + .pipe(Effect.forkChild) + yield* Deferred.await(started) + yield* llm.text("second") + + const id = MessageID.ascending() + const queued = yield* prompt + .prompt({ + sessionID: chat.id, + messageID: id, + agent: "build", + model: ref, + parts: [{ type: "text", text: "second" }], + }) + .pipe(Effect.forkChild) + yield* pollWithTimeout( + sessions + .messages({ sessionID: chat.id }) + .pipe(Effect.map((msgs) => (msgs.some((msg) => msg.info.id === id) ? true : undefined))), + "timed out waiting for queued prompt to save", + ) + + yield* Deferred.succeed(release, void 0) + yield* Fiber.await(active) + const exit = yield* Fiber.await(queued) + expect(Exit.isSuccess(exit)).toBe(true) + if (!Exit.isSuccess(exit)) return + expect(exit.value.info.role).toBe("assistant") + if (exit.value.info.role !== "assistant") return + expect(exit.value.info.parentID).toBe(id) + expect(exit.value.parts.some((part) => part.type === "text" && part.text === "second")).toBe(true) + expect(yield* llm.calls).toBe(1) + }), + 3_000, +) + it.instance( "assertNotBusy fails with BusyError when loop running", () =>