diff --git a/packages/opencode/src/question/index.ts b/packages/opencode/src/question/index.ts index 488ffeeec745..97669392cc20 100644 --- a/packages/opencode/src/question/index.ts +++ b/packages/opencode/src/question/index.ts @@ -68,10 +68,6 @@ export interface Interface { requestID: QuestionID answers: ReadonlyArray }) => Effect.Effect - readonly saveProgress: (input: { - requestID: QuestionID - answers: ReadonlyArray - }) => Effect.Effect readonly reject: (requestID: QuestionID) => Effect.Effect readonly rejectAllForSession: (sessionID: SessionID) => Effect.Effect readonly list: () => Effect.Effect> @@ -143,21 +139,19 @@ const layer = Layer.effect( // Deleting the row is the claim: whichever concurrent reply/reject wins the delete owns the request. // Scoped to this instance's project (like list) so co-located instances sharing the global DB // cannot consume each other's live requests. - const inProject = Effect.fn("Question.inProject")(function* (requestID: QuestionID) { - const ctx = yield* InstanceState.context - return and( - eq(QuestionRequestTable.id, requestID), - inArray( - QuestionRequestTable.session_id, - db.select({ id: SessionTable.id }).from(SessionTable).where(eq(SessionTable.project_id, ctx.project.id)), - ), - ) - }) - const claim = Effect.fn("Question.claim")(function* (requestID: QuestionID) { + const ctx = yield* InstanceState.context return yield* db .delete(QuestionRequestTable) - .where(yield* inProject(requestID)) + .where( + and( + eq(QuestionRequestTable.id, requestID), + inArray( + QuestionRequestTable.session_id, + db.select({ id: SessionTable.id }).from(SessionTable).where(eq(SessionTable.project_id, ctx.project.id)), + ), + ), + ) .returning() .get() .pipe(Effect.orDie) @@ -195,26 +189,6 @@ const layer = Layer.effect( return { outcome: "resolved" } as const }) - // Partial answers, so a person can finish a multi-step question from any client, or after a reload. - const saveProgress = Effect.fn("Question.saveProgress")(function* (input: { - requestID: QuestionID - answers: ReadonlyArray - }) { - const where = yield* inProject(input.requestID) - const row = yield* db.select().from(QuestionRequestTable).where(where).get().pipe(Effect.orDie) - // A reply can claim the row between the read and the write; the write then matches nothing. - const saved = - row && - (yield* db - .update(QuestionRequestTable) - .set({ data: { ...row.data, progress: input.answers.map((a) => [...a]) } }) - .where(where) - .returning({ id: QuestionRequestTable.id }) - .get() - .pipe(Effect.orDie)) - if (!saved) return yield* new NotFoundError({ requestID: input.requestID }) - }) - const rejectClaimed = Effect.fn("Question.rejectClaimed")(function* (row: QuestionRequestRow) { yield* Effect.logInfo("rejected", { requestID: row.id }) yield* events.publish(Event.Rejected, { @@ -260,7 +234,7 @@ const layer = Layer.effect( return rows.map((x) => rowToRequest(x.request)) }) - return Service.of({ ask, reply, saveProgress, reject, rejectAllForSession, list }) + return Service.of({ ask, reply, reject, rejectAllForSession, list }) }), ) diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/question.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/question.ts index b59f902332ea..e9e63429db20 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/question.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/question.ts @@ -42,19 +42,6 @@ export const QuestionApi = HttpApi.make("question") description: "Provide answers to a question request from the AI assistant.", }), ), - HttpApiEndpoint.put("progress", `${root}/:requestID/progress`, { - params: { requestID: QuestionID }, - query: WorkspaceRoutingQuery, - payload: ReplyPayload, - success: described(Schema.Boolean, "Progress saved successfully"), - error: [HttpApiError.BadRequest, QuestionNotFoundError], - }).annotateMerge( - OpenApi.annotations({ - identifier: "question.progress", - summary: "Save question progress", - description: "Save the answers given so far without replying, so any client can show them.", - }), - ), HttpApiEndpoint.post("reject", `${root}/:requestID/reject`, { params: { requestID: QuestionID }, query: WorkspaceRoutingQuery, diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/question.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/question.ts index 7fe360d2aaad..b6c0d4e0086d 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/question.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/question.ts @@ -46,23 +46,6 @@ export const questionHandlers = HttpApiBuilder.group(InstanceHttpApi, "question" return true }) - const progress = Effect.fn("QuestionHttpApi.progress")(function* (ctx: { - params: { requestID: QuestionID } - payload: Question.Reply - }) { - yield* svc.saveProgress({ requestID: ctx.params.requestID, answers: ctx.payload.answers }).pipe( - Effect.catchTag("Question.NotFoundError", (error) => - Effect.fail( - new QuestionNotFoundError({ - requestID: String(error.requestID), - message: `Question request not found: ${error.requestID}`, - }), - ), - ), - ) - return true - }) - const reject = Effect.fn("QuestionHttpApi.reject")(function* (ctx: { params: { requestID: QuestionID } }) { yield* svc.reject(ctx.params.requestID).pipe( Effect.catchTag("Question.NotFoundError", (error) => @@ -77,6 +60,6 @@ export const questionHandlers = HttpApiBuilder.group(InstanceHttpApi, "question" return true }) - return handlers.handle("list", list).handle("reply", reply).handle("progress", progress).handle("reject", reject) + return handlers.handle("list", list).handle("reply", reply).handle("reject", reject) }), ) diff --git a/packages/opencode/test/question/question.test.ts b/packages/opencode/test/question/question.test.ts index dc5f47c9cd96..760f6148efe1 100644 --- a/packages/opencode/test/question/question.test.ts +++ b/packages/opencode/test/question/question.test.ts @@ -43,14 +43,6 @@ const rejectEffect = Effect.fn("QuestionTest.reject")(function* (id: QuestionID) yield* question.reject(id) }) -const saveProgressEffect = Effect.fn("QuestionTest.saveProgress")(function* (input: { - requestID: QuestionID - answers: ReadonlyArray -}) { - const question = yield* Question.Service - yield* question.saveProgress(input) -}) - afterEach(async () => { await disposeAllInstances() }) @@ -511,7 +503,6 @@ lifecycle.live("request survives instance reload and reply heals it", () => }).pipe(provideInstance(dir), Effect.forkScoped) const pending = yield* waitForPending(1).pipe(provideInstance(dir)) - yield* saveProgressEffect({ requestID: pending[0].id, answers: [["Yes"]] }).pipe(provideInstance(dir)) yield* InstanceStore.Service.use((store) => store.reload({ directory: dir })) yield* Fiber.await(fiber) @@ -520,7 +511,6 @@ lifecycle.live("request survives instance reload and reply heals it", () => expect(survived[0].id).toBe(pending[0].id) expect(survived[0].sessionID).toBe(SessionID.make("ses_survive")) expect(survived[0].questions).toEqual(questions) - expect(survived[0].progress).toEqual([["Yes"]]) const replied = yield* Effect.gen(function* () { const events = yield* EventV2Bridge.Service @@ -540,32 +530,3 @@ lifecycle.live("request survives instance reload and reply heals it", () => expect(after).toHaveLength(0) }), ) - -// progress - -it.instance( - "saveProgress - lists partial answers, and fails once the question is answered", - () => - Effect.gen(function* () { - const fiber = yield* askEffect({ - sessionID: SessionID.make("ses_progress"), - questions: [ - { question: "Which layout?", header: "Layout", options: [{ label: "Bold", description: "Big image" }] }, - { question: "Which color?", header: "Color", options: [{ label: "Red", description: "Warm" }] }, - ], - }).pipe(Effect.forkScoped) - const [request] = yield* waitForPending(1) - // Clients that never save progress see exactly the payload they did before. - expect(request).not.toHaveProperty("progress") - - yield* saveProgressEffect({ requestID: request.id, answers: [["Bold"], []] }) - expect((yield* listEffect)[0]?.progress).toEqual([["Bold"], []]) - - yield* replyEffect({ requestID: request.id, answers: [["Bold"], ["Red"]] }) - expect(yield* Fiber.join(fiber)).toEqual([["Bold"], ["Red"]]) - const exit = yield* saveProgressEffect({ requestID: request.id, answers: [["Bold"], ["Red"]] }).pipe(Effect.exit) - expect(Exit.isFailure(exit)).toBe(true) - if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Question.NotFoundError) - }), - { git: true }, -) diff --git a/packages/opencode/test/server/httpapi-exercise/index.ts b/packages/opencode/test/server/httpapi-exercise/index.ts index dfca89276770..c8e11ea8451b 100644 --- a/packages/opencode/test/server/httpapi-exercise/index.ts +++ b/packages/opencode/test/server/httpapi-exercise/index.ts @@ -313,43 +313,6 @@ const scenarios: Scenario[] = [ body: { answers: [["Yes"]] }, })) .json(404, object, "status"), - http.protected - .put("/question/{requestID}/progress", "question.progress.invalid") - .at((ctx) => ({ - path: route("/question/{requestID}/progress", { requestID: "que_httpapi_progress" }), - headers: ctx.headers(), - body: { answers: "Yes" }, - })) - .status(400), - http.protected - .put("/question/{requestID}/progress", "question.progress.missing") - .at((ctx) => ({ - path: route("/question/{requestID}/progress", { requestID: "que_httpapi_progress" }), - headers: ctx.headers(), - body: { answers: [["Yes"]] }, - })) - .json(404, object, "status"), - http.protected - .put("/question/{requestID}/progress", "question.progress") - .mutating() - .seeded((ctx) => - Effect.gen(function* () { - const session = yield* ctx.session({ title: "Question progress owner" }) - return yield* ctx.question(session.id) - }), - ) - .at((ctx) => ({ - path: route("/question/{requestID}/progress", { requestID: ctx.state }), - headers: ctx.headers(), - body: { answers: [["Yes"]] }, - })) - .jsonEffect(200, (body, ctx) => - Effect.gen(function* () { - check(body === true, "question progress should return true") - const saved = (yield* ctx.questions()).find((request) => request.id === ctx.state) - check(stable(saved?.progress) === stable([["Yes"]]), "saved progress should be listed with its question") - }), - ), http.protected .post("/question/{requestID}/reject", "question.reject") .at((ctx) => ({ diff --git a/packages/opencode/test/server/httpapi-exercise/runner.ts b/packages/opencode/test/server/httpapi-exercise/runner.ts index 13f16117fce3..b7ad6d62083b 100644 --- a/packages/opencode/test/server/httpapi-exercise/runner.ts +++ b/packages/opencode/test/server/httpapi-exercise/runner.ts @@ -7,7 +7,6 @@ import type { Config } from "../../../src/config/config" import type { MessageV2 } from "../../../src/session/message-v2" import { MessageID, PartID } from "../../../src/session/schema" -import { QuestionID } from "../../../src/question/schema" import { call, callAuthProbe, disposeApps } from "./backend" import { original } from "./environment" import { runtime } from "./runtime" @@ -178,24 +177,6 @@ function withContext( messages: (sessionID) => run(modules.Session.Service.use((svc) => svc.messages({ sessionID }).pipe(Effect.orDie))), todos: (sessionID, todos) => run(modules.Todo.Service.use((svc) => svc.update({ sessionID, todos }))), - // Inserts the row directly: a real ask blocks until someone answers it. - question: (sessionID) => - run( - Effect.gen(function* () { - const { db } = yield* modules.Database.Service - const id = QuestionID.ascending() - const questions = [ - { question: "Ship it?", header: "Ship", options: [{ label: "Yes", description: "Ship" }] }, - ] - yield* db - .insert(modules.QuestionRequestTable) - .values({ id, session_id: sessionID, data: { questions } }) - .run() - .pipe(Effect.orDie) - return id - }), - ), - questions: () => run(modules.Question.Service.use((svc) => svc.list())), worktree: (input) => run(modules.Worktree.Service.use((svc) => svc.create(input).pipe(Effect.orDie))), worktreeRemove: (directory) => run(modules.Worktree.Service.use((svc) => svc.remove({ directory })).pipe(Effect.ignore)), diff --git a/packages/opencode/test/server/httpapi-exercise/runtime.ts b/packages/opencode/test/server/httpapi-exercise/runtime.ts index 3c3319952912..81bd302a1a11 100644 --- a/packages/opencode/test/server/httpapi-exercise/runtime.ts +++ b/packages/opencode/test/server/httpapi-exercise/runtime.ts @@ -7,9 +7,6 @@ export type Runtime = { InstanceStore: (typeof import("../../../src/project/instance-store"))["InstanceStore"] Session: (typeof import("../../../src/session/session"))["Session"] Todo: (typeof import("../../../src/session/todo"))["Todo"] - Question: (typeof import("../../../src/question"))["Question"] - Database: (typeof import("@opencode-ai/core/database/database"))["Database"] - QuestionRequestTable: (typeof import("@opencode-ai/core/session/sql"))["QuestionRequestTable"] Worktree: (typeof import("../../../src/worktree"))["Worktree"] Project: (typeof import("../../../src/project/project"))["Project"] Tui: typeof import("../../../src/server/shared/tui-control") @@ -30,9 +27,6 @@ export function runtime() { const instanceStore = await import("../../../src/project/instance-store") const session = await import("../../../src/session/session") const todo = await import("../../../src/session/todo") - const question = await import("../../../src/question") - const database = await import("@opencode-ai/core/database/database") - const sql = await import("@opencode-ai/core/session/sql") const worktree = await import("../../../src/worktree") const project = await import("../../../src/project/project") const tui = await import("../../../src/server/shared/tui-control") @@ -47,9 +41,6 @@ export function runtime() { InstanceStore: instanceStore.InstanceStore, Session: session.Session, Todo: todo.Todo, - Question: question.Question, - Database: database.Database, - QuestionRequestTable: sql.QuestionRequestTable, Worktree: worktree.Worktree, Project: project.Project, Tui: tui, diff --git a/packages/opencode/test/server/httpapi-exercise/types.ts b/packages/opencode/test/server/httpapi-exercise/types.ts index d8b66a412235..acc1898e3285 100644 --- a/packages/opencode/test/server/httpapi-exercise/types.ts +++ b/packages/opencode/test/server/httpapi-exercise/types.ts @@ -6,8 +6,6 @@ import type { Project } from "../../../src/project/project" import type { Worktree } from "../../../src/worktree" import type { MessageV2 } from "../../../src/session/message-v2" import type { SessionID } from "../../../src/session/schema" -import type { Question } from "../../../src/question" -import type { QuestionID } from "../../../src/question/schema" export const OpenApiMethods = ["get", "post", "put", "delete", "patch"] as const export const Methods = ["GET", "POST", "PUT", "DELETE", "PATCH"] as const @@ -63,8 +61,6 @@ export type ScenarioContext = { message: (sessionID: SessionID, input?: { text?: string }) => Effect.Effect messages: (sessionID: SessionID) => Effect.Effect todos: (sessionID: SessionID, todos: TodoInfo[]) => Effect.Effect - question: (sessionID: SessionID) => Effect.Effect - questions: () => Effect.Effect> worktree: (input?: { name?: string }) => Effect.Effect worktreeRemove: (directory: string) => Effect.Effect llmText: (value: string) => Effect.Effect diff --git a/packages/schema/src/v1/question.ts b/packages/schema/src/v1/question.ts index 4652b31c11b3..e5669964726d 100644 --- a/packages/schema/src/v1/question.ts +++ b/packages/schema/src/v1/question.ts @@ -17,8 +17,6 @@ export const Option = Schema.Struct({ description: Schema.String.annotate({ description: "Explanation of choice" }), }).annotate({ identifier: "QuestionOption" }) -export const Answer = Schema.Array(Schema.String).annotate({ identifier: "QuestionAnswer" }) - const base = { question: Schema.String.annotate({ description: "Complete question" }), header: Schema.String.annotate({ description: "Very short label (max 30 chars)" }), @@ -39,10 +37,8 @@ export const Request = Schema.Struct({ sessionID: SessionID, questions: Schema.Array(Info).annotate({ description: "Questions to ask" }), tool: Schema.optional(Tool), - progress: Schema.optional(Schema.Array(Answer)).annotate({ - description: "Answers saved before the reply, in question order (empty for a question not answered yet)", - }), }).annotate({ identifier: "QuestionRequest" }) +export const Answer = Schema.Array(Schema.String).annotate({ identifier: "QuestionAnswer" }) export const Reply = Schema.Struct({ answers: Schema.Array(Answer).annotate({ description: "User answers in order of questions (each answer is an array of selected labels)", diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index f7ae7b186a6d..a2bcd4252c6d 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -170,8 +170,6 @@ import type { QuestionAnswer, QuestionListErrors, QuestionListResponses, - QuestionProgressErrors, - QuestionProgressResponses, QuestionRejectErrors, QuestionRejectResponses, QuestionReplyErrors, @@ -3051,45 +3049,6 @@ export class Question extends HeyApiClient { }) } - /** - * Save question progress - * - * Save the answers given so far without replying, so any client can show them. - */ - public progress( - parameters: { - requestID: string - directory?: string - workspace?: string - answers?: Array - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "requestID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "answers" }, - ], - }, - ], - ) - return (options?.client ?? this.client).put({ - url: "/question/{requestID}/progress", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - /** * Reject question request * diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index f1c3cd750ee0..f06c20cc413e 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1516,7 +1516,6 @@ export type GlobalEvent = { */ questions: Array tool?: QuestionTool - progress?: Array } } | { @@ -1747,7 +1746,6 @@ export type ProviderConfig = { baseURL?: string enterpriseUrl?: string setCacheKey?: boolean - promptCacheKey?: string /** * Timeout in milliseconds for full requests to this provider. Set to false to disable timeout. */ @@ -2462,7 +2460,6 @@ export type QuestionRequest = { */ questions: Array tool?: QuestionTool - progress?: Array } export type QuestionNotFoundError = { @@ -5953,7 +5950,6 @@ export type QuestionAsked = { */ questions: Array tool?: QuestionTool - progress?: Array } } @@ -6963,7 +6959,6 @@ export type EventQuestionAsked = { */ questions: Array tool?: QuestionTool - progress?: Array } } @@ -9207,45 +9202,6 @@ export type QuestionReplyResponses = { export type QuestionReplyResponse = QuestionReplyResponses[keyof QuestionReplyResponses] -export type QuestionProgressData = { - body?: { - /** - * User answers in order of questions (each answer is an array of selected labels) - */ - answers: Array - } - path: { - requestID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/question/{requestID}/progress" -} - -export type QuestionProgressErrors = { - /** - * BadRequest | InvalidRequestError - */ - 400: EffectHttpApiErrorBadRequest | InvalidRequestError - /** - * QuestionNotFoundError - */ - 404: QuestionNotFoundError -} - -export type QuestionProgressError = QuestionProgressErrors[keyof QuestionProgressErrors] - -export type QuestionProgressResponses = { - /** - * Progress saved successfully - */ - 200: boolean -} - -export type QuestionProgressResponse = QuestionProgressResponses[keyof QuestionProgressResponses] - export type QuestionRejectData = { body?: never path: { diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index beaa0be93752..e66d14050558 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -4728,107 +4728,6 @@ ] } }, - "/question/{requestID}/progress": { - "put": { - "tags": ["question"], - "operationId": "question.progress", - "parameters": [ - { - "name": "requestID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^que" - }, - "required": true - }, - { - "name": "directory", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - }, - { - "name": "workspace", - "in": "query", - "schema": { - "type": "string" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "Progress saved successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean", - "description": "Progress saved successfully" - } - } - } - }, - "400": { - "description": "BadRequest | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/effect_HttpApiError_BadRequest" - }, - { - "$ref": "#/components/schemas/InvalidRequestError" - } - ] - } - } - } - }, - "404": { - "description": "QuestionNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/QuestionNotFoundError" - } - } - } - } - }, - "description": "Save the answers given so far without replying, so any client can show them.", - "summary": "Save question progress", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "answers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionAnswer" - }, - "description": "User answers in order of questions (each answer is an array of selected labels)" - } - }, - "required": ["answers"], - "additionalProperties": false - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.question.progress({\n ...\n})" - } - ] - } - }, "/question/{requestID}/reject": { "post": { "tags": ["question"], @@ -20221,12 +20120,6 @@ }, "tool": { "$ref": "#/components/schemas/QuestionTool" - }, - "progress": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionAnswer" - } } }, "required": ["id", "sessionID", "questions"], @@ -20863,9 +20756,6 @@ "setCacheKey": { "type": "boolean" }, - "promptCacheKey": { - "type": "string" - }, "timeout": { "anyOf": [ { @@ -22935,12 +22825,6 @@ }, "tool": { "$ref": "#/components/schemas/QuestionTool" - }, - "progress": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionAnswer" - } } }, "required": ["id", "sessionID", "questions"], @@ -33398,12 +33282,6 @@ }, "tool": { "$ref": "#/components/schemas/QuestionTool" - }, - "progress": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionAnswer" - } } }, "required": ["id", "sessionID", "questions"], @@ -36516,12 +36394,6 @@ }, "tool": { "$ref": "#/components/schemas/QuestionTool" - }, - "progress": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionAnswer" - } } }, "required": ["id", "sessionID", "questions"],