From 82071ff90c6830bfd683d2bf343e8412e339c275 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Thu, 21 May 2026 21:57:37 +0530 Subject: [PATCH] fix(httpapi): record deferred v2 prompts --- .../instance/httpapi/groups/v2/session.ts | 2 +- packages/opencode/src/v2/session.ts | 45 ++++++++++++++++++- .../test/server/httpapi-session.test.ts | 31 +++++++++++++ 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts index c1a07957db..e180c1c5ed 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts @@ -72,7 +72,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session") OpenApi.annotations({ identifier: "v2.session.prompt", summary: "Send v2 message", - description: "Create a v2 session message and queue it for the agent loop.", + description: "Create a deferred v2 user message. Immediate agent delivery is not available yet.", }), ), ) diff --git a/packages/opencode/src/v2/session.ts b/packages/opencode/src/v2/session.ts index dcff1a318a..9223cff214 100644 --- a/packages/opencode/src/v2/session.ts +++ b/packages/opencode/src/v2/session.ts @@ -314,7 +314,50 @@ export const layer = Layer.effect( }), prompt: Effect.fn("V2Session.prompt")(function* (input) { yield* result.get(input.sessionID) - return yield* new OperationUnavailableError({ operation: "prompt" }) + if (input.delivery !== "deferred") return yield* new OperationUnavailableError({ operation: "prompt" }) + const event = yield* events.publish( + SessionEvent.Prompted, + { + sessionID: input.sessionID, + timestamp: DateTime.makeUnsafe(Date.now()), + prompt: input.prompt, + }, + { id: input.id }, + ) + const message = new SessionMessage.User({ + id: event.id, + type: "user", + metadata: event.metadata, + text: event.data.prompt.text, + files: event.data.prompt.files, + agents: event.data.prompt.agents, + references: event.data.prompt.references, + time: { created: event.data.timestamp }, + }) + // The bridge currently publishes the event but does not guarantee this request path has a projector attached. + Database.use((db) => { + const existing = db.select().from(SessionMessageTable).where(eq(SessionMessageTable.id, message.id)).get() + if (existing) return + db.insert(SessionMessageTable) + .values([ + { + id: message.id, + session_id: input.sessionID, + type: message.type, + time_created: DateTime.toEpochMillis(message.time.created), + data: { + metadata: message.metadata, + text: message.text, + files: message.files, + agents: message.agents, + references: message.references, + time: { created: DateTime.toEpochMillis(message.time.created) }, + } as NonNullable<(typeof SessionMessageTable.$inferInsert)["data"]>, + }, + ]) + .run() + }) + return message }), shell: Effect.fn("V2Session.shell")(function* (_input) {}), skill: Effect.fn("V2Session.skill")(function* (_input) {}), diff --git a/packages/opencode/test/server/httpapi-session.test.ts b/packages/opencode/test/server/httpapi-session.test.ts index cdb50227ef..cd5c3da23b 100644 --- a/packages/opencode/test/server/httpapi-session.test.ts +++ b/packages/opencode/test/server/httpapi-session.test.ts @@ -460,6 +460,37 @@ describe("session HttpApi", () => { { git: true, config: { formatter: false, lsp: false } }, ) + it.instance( + "records deferred v2 prompts as projected user messages", + () => + Effect.gen(function* () { + const test = yield* TestInstance + const headers = { "x-opencode-directory": test.directory } + const session = yield* createSession({ title: "v2 deferred prompt" }) + + const prompt = yield* request(`/api/session/${session.id}/prompt`, { + method: "POST", + headers: { ...headers, "content-type": "application/json" }, + body: JSON.stringify({ prompt: { text: "hello deferred" }, delivery: "deferred" }), + }) + expect(prompt.status).toBe(200) + const promptBody = yield* responseJson(prompt) + expect(promptBody).toMatchObject({ + type: "user", + text: "hello deferred", + }) + + const messages = yield* requestJson<{ items: SessionMessage.Message[] }>(`/api/session/${session.id}/message`, { + headers, + }) + expect(messages.items).toMatchObject([{ type: "user", text: "hello deferred" }]) + + const context = yield* requestJson(`/api/session/${session.id}/context`, { headers }) + expect(context).toMatchObject([{ type: "user", text: "hello deferred" }]) + }), + { git: true, config: { formatter: false, lsp: false } }, + ) + it.instance( "returns v2 public unavailable errors for unfinished session mutations", () =>