fix(httpapi): record deferred v2 prompts

This commit is contained in:
Shoubhit Dash 2026-05-21 21:57:37 +05:30
parent 61390dbb49
commit 82071ff90c
3 changed files with 76 additions and 2 deletions

View file

@ -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.",
}),
),
)

View file

@ -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) {}),

View file

@ -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<SessionMessage.Message[]>(`/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",
() =>