fix(httpapi): align session boolean query parsing (#24693)

This commit is contained in:
Kit Langton
2026-04-27 20:53:27 -04:00
committed by GitHub
parent c103202ad5
commit faca24d487
5 changed files with 147 additions and 91 deletions

View File

@@ -37,6 +37,8 @@ const ConsoleSwitchBody = z.object({
orgID: z.string(),
})
const QueryBoolean = z.enum(["true", "false"]).transform((value) => value === "true")
export const ExperimentalRoutes = lazy(() =>
new Hono()
.get(
@@ -346,7 +348,7 @@ export const ExperimentalRoutes = lazy(() =>
"query",
z.object({
directory: z.string().optional().meta({ description: "Filter sessions by project directory" }),
roots: z.coerce.boolean().optional().meta({ description: "Only return root sessions (no parentID)" }),
roots: QueryBoolean.optional().meta({ description: "Only return root sessions (no parentID)" }),
start: z.coerce
.number()
.optional()
@@ -357,7 +359,7 @@ export const ExperimentalRoutes = lazy(() =>
.meta({ description: "Return sessions updated before this timestamp (milliseconds since epoch)" }),
search: z.string().optional().meta({ description: "Filter sessions by title (case-insensitive)" }),
limit: z.coerce.number().optional().meta({ description: "Maximum number of sessions to return" }),
archived: z.coerce.boolean().optional().meta({ description: "Include archived sessions (default false)" }),
archived: QueryBoolean.optional().meta({ description: "Include archived sessions (default false)" }),
}),
),
async (c) => {

View File

@@ -10,7 +10,7 @@ import { Session } from "@/session/session"
import { ToolRegistry } from "@/tool/registry"
import * as EffectZod from "@/util/effect-zod"
import { Worktree } from "@/worktree"
import { Effect, Layer, Option, Schema } from "effect"
import { Effect, Layer, Option, Schema, SchemaGetter } from "effect"
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "./auth"
@@ -51,15 +51,21 @@ const ToolListQuery = Schema.Struct({
model: ModelID,
})
const QueryBoolean = Schema.Literals(["true", "false"]).pipe(
Schema.decodeTo(Schema.Boolean, {
decode: SchemaGetter.transform((value) => value === "true"),
encode: SchemaGetter.transform((value) => (value ? "true" : "false")),
}),
)
const WorktreeList = Schema.Array(Schema.String).annotate({ identifier: "WorktreeList" })
const SessionListQuery = Schema.Struct({
directory: Schema.optional(Schema.String),
roots: Schema.optional(Schema.Literals(["true", "false"])),
roots: Schema.optional(QueryBoolean),
start: Schema.optional(Schema.NumberFromString),
cursor: Schema.optional(Schema.NumberFromString),
search: Schema.optional(Schema.String),
limit: Schema.optional(Schema.NumberFromString),
archived: Schema.optional(Schema.Literals(["true", "false"])),
archived: Schema.optional(QueryBoolean),
})
export const ExperimentalPaths = {
@@ -307,12 +313,12 @@ export const experimentalHandlers = Layer.unwrap(
const sessions = Array.from(
Session.listGlobal({
directory: ctx.query.directory,
roots: ctx.query.roots === "true" ? true : undefined,
roots: ctx.query.roots,
start: ctx.query.start,
cursor: ctx.query.cursor,
search: ctx.query.search,
limit: limit + 1,
archived: ctx.query.archived === "true" ? true : undefined,
archived: ctx.query.archived,
}),
)
const list = sessions.length > limit ? sessions.slice(0, limit) : sessions

View File

@@ -21,7 +21,7 @@ import { MessageID, PartID, SessionID } from "@/session/schema"
import { Snapshot } from "@/snapshot"
import * as Log from "@opencode-ai/core/util/log"
import { NamedError } from "@opencode-ai/core/util/error"
import { Effect, Layer, Schema, Struct } from "effect"
import { Effect, Layer, Schema, SchemaGetter, Struct } from "effect"
import * as Stream from "effect/Stream"
import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
import {
@@ -37,9 +37,15 @@ import { Authorization } from "./auth"
const log = Log.create({ service: "server" })
const root = "/session"
const QueryBoolean = Schema.Literals(["true", "false"]).pipe(
Schema.decodeTo(Schema.Boolean, {
decode: SchemaGetter.transform((value) => value === "true"),
encode: SchemaGetter.transform((value) => (value ? "true" : "false")),
}),
)
const ListQuery = Schema.Struct({
directory: Schema.optional(Schema.String),
roots: Schema.optional(Schema.Literals(["true", "false"])),
roots: Schema.optional(QueryBoolean),
start: Schema.optional(Schema.NumberFromString),
search: Schema.optional(Schema.String),
limit: Schema.optional(Schema.NumberFromString),
@@ -436,7 +442,7 @@ export const sessionHandlers = Layer.unwrap(
Array.from(
Session.list({
directory: ctx.query.directory,
roots: ctx.query.roots === "true" ? true : undefined,
roots: ctx.query.roots,
start: ctx.query.start,
search: ctx.query.search,
limit: ctx.query.limit,
@@ -472,8 +478,8 @@ export const sessionHandlers = Layer.unwrap(
params: { sessionID: SessionID }
query: typeof MessagesQuery.Type
}) {
if (ctx.query.before !== undefined && ctx.query.limit === undefined) return yield* new HttpApiError.BadRequest({})
if (ctx.query.before !== undefined) {
if (ctx.query.before && ctx.query.limit === undefined) return yield* new HttpApiError.BadRequest({})
if (ctx.query.before) {
const before = ctx.query.before
yield* Effect.try({
try: () => MessageV2.cursor.decode(before),

View File

@@ -30,6 +30,8 @@ import { jsonRequest, runRequest } from "./trace"
const log = Log.create({ service: "server" })
const QueryBoolean = z.enum(["true", "false"]).transform((value) => value === "true")
export const SessionRoutes = lazy(() =>
new Hono()
.get(
@@ -53,7 +55,7 @@ export const SessionRoutes = lazy(() =>
"query",
z.object({
directory: z.string().optional().meta({ description: "Filter sessions by project directory" }),
roots: z.coerce.boolean().optional().meta({ description: "Only return root sessions (no parentID)" }),
roots: QueryBoolean.optional().meta({ description: "Only return root sessions (no parentID)" }),
start: z.coerce
.number()
.optional()