chore: merge origin/dev into v2

Brings the latest dev (45 commits) into v2. Conflict resolutions:

- core/src/session.ts: keep v2's Service (BusyError active-checks in
  revert.stage/clear/commit, rename method + Renamed event) and add
  dev's history method. v2's Renamed event flows through the merged
  SessionEvent.Definitions into the OpenCodeEvent union, so it encodes
  correctly on the SSE stream.
- server/src/handlers/event.ts: take dev's OpenCodeEvent encoder, which
  supersedes v2's PublicEventManifest handler wiring. This also brings
  dev's live event stream (allBounded + heartbeat + keepalive headers)
  onto v2.
- Removed the now-orphaned public-event-manifest.ts (only self-referenced
  after taking dev's handler).
- Generated client + sdk types regenerated via codegen (not hand-merged);
  they pick up dev's effect SSE OpenAPI-naming patch (V2EventStream).
This commit is contained in:
Kit Langton 2026-06-27 22:41:08 -04:00
commit 727f9f46a0
207 changed files with 9806 additions and 6648 deletions

View file

@ -3,7 +3,7 @@ import { EventManifest } from "@opencode-ai/schema/event-manifest"
import { Location } from "@opencode-ai/schema/location"
import type { Definition } from "@opencode-ai/schema/event"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
const fields = {
id: Event.ID,
@ -12,15 +12,9 @@ const fields = {
location: Schema.optional(Location.Ref),
}
const schema = (definitions: ReadonlyArray<Definition>) =>
const schema = <const Definitions extends ReadonlyArray<Definition>>(definitions: Definitions) =>
Schema.Union([
...definitions.map((definition) =>
Schema.Struct({
...fields,
type: Schema.Literal(definition.type),
data: definition.data,
}).annotate({ identifier: `V2Event.${definition.type}` }),
),
...definitions,
...(definitions.some((definition) => definition.type === "server.connected")
? []
: [
@ -32,14 +26,14 @@ const schema = (definitions: ReadonlyArray<Definition>) =>
]),
]).annotate({ identifier: "V2Event" })
const make = (definitions: ReadonlyArray<Definition>) => {
const make = <const Definitions extends ReadonlyArray<Definition>>(definitions: Definitions) => {
const EventSchema = schema(definitions)
return {
schema: EventSchema,
group: HttpApiGroup.make("server.event")
.add(
HttpApiEndpoint.get("event.subscribe", "/api/event", {
success: EventSchema,
success: HttpApiSchema.StreamSse({ data: EventSchema }),
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.event.subscribe",
@ -52,8 +46,11 @@ const make = (definitions: ReadonlyArray<Definition>) => {
}
}
export const makeEventGroup = (definitions: ReadonlyArray<Definition>) => make(definitions).group
export const makeEventGroup = <const Definitions extends ReadonlyArray<Definition>>(definitions: Definitions) =>
make(definitions).group
const event = make(EventManifest.ServerDefinitions)
export const EventGroup = event.group
export type Event = typeof event.schema.Type
export const OpenCodeEvent = event.schema
export type OpenCodeEvent = typeof OpenCodeEvent.Type
export type OpenCodeEventEncoded = typeof OpenCodeEvent.Encoded

View file

@ -5,7 +5,7 @@ import { Session } from "@opencode-ai/schema/session"
import { Project } from "@opencode-ai/schema/project"
import { AbsolutePath, NonNegativeInt, PositiveInt, RelativePath, statics } from "@opencode-ai/schema/schema"
import { Workspace } from "@opencode-ai/schema/workspace"
import { Context, Encoding, Result, Schema, Struct } from "effect"
import { Context, Effect, Encoding, Result, Schema, Struct } from "effect"
import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import {
ConflictError,
@ -61,6 +61,7 @@ const SessionsCursorInput = Schema.Union([
const SessionsCursorJson = Schema.fromJsonString(SessionsCursorInput)
const encodeSessionsCursor = Schema.encodeSync(SessionsCursorJson)
const decodeSessionsCursor = Schema.decodeUnknownEffect(SessionsCursorJson)
const invalidCursor = "Invalid cursor" as const
export const SessionsCursor = Schema.String.pipe(
Schema.brand("SessionsCursor"),
@ -68,7 +69,13 @@ export const SessionsCursor = Schema.String.pipe(
const make = schema.make.bind(schema)
return {
make: (input: typeof SessionsCursorInput.Type) => make(Encoding.encodeBase64Url(encodeSessionsCursor(input))),
parse: (input: string) => decodeSessionsCursor(Result.getOrThrow(Encoding.decodeBase64UrlString(input))),
parse: (input: string) =>
Effect.suspend(() => {
const result = Encoding.decodeBase64UrlString(input)
return Result.isFailure(result)
? Effect.fail(invalidCursor)
: decodeSessionsCursor(result.success).pipe(Effect.mapError(() => invalidCursor))
}),
}
}),
)
@ -78,6 +85,13 @@ const SessionActive = Schema.Struct({
type: Schema.Literal("running"),
}).annotate({ identifier: "SessionActive" })
const SessionHistoryLimit = PositiveInt.check(Schema.isLessThanOrEqualTo(100))
export const SessionHistoryQuery = Schema.Struct({
limit: Schema.NumberFromString.pipe(Schema.decodeTo(SessionHistoryLimit), Schema.optional),
after: Schema.NumberFromString.pipe(Schema.decodeTo(NonNegativeInt), Schema.optional),
})
const SessionsQueryCursor = SessionsCursor.annotate({
description: "Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response.",
})
@ -306,6 +320,26 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
}),
),
)
.add(
HttpApiEndpoint.get("session.history", "/api/session/:sessionID/history", {
params: { sessionID: Session.ID },
query: SessionHistoryQuery,
success: Schema.Struct({
data: Schema.Array(SessionEvent.Durable),
hasMore: Schema.Boolean,
}).annotate({ identifier: "SessionHistory" }),
error: SessionNotFoundError,
})
.middleware(sessionLocationMiddleware)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.session.history",
summary: "Get session history",
description:
"Read one finite page of public durable Session events after an exclusive aggregate sequence. Newly committed events may appear on later pages.",
}),
),
)
.add(
HttpApiEndpoint.get("session.events", "/api/session/:sessionID/event", {
params: { sessionID: Session.ID },

View file

@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test"
import { Effect } from "effect"
import { SessionsCursor } from "../src/groups/session"
import { Effect, Schema } from "effect"
import { SessionHistoryQuery, SessionsCursor } from "../src/groups/session"
import { Session } from "@opencode-ai/schema/session"
describe("SessionsCursor", () => {
@ -16,3 +16,11 @@ describe("SessionsCursor", () => {
expect(await Effect.runPromise(SessionsCursor.parse(cursor))).toEqual(input)
})
})
describe("SessionHistoryQuery", () => {
test("decodes numeric paging inputs", async () => {
const query = await Effect.runPromise(Schema.decodeUnknownEffect(SessionHistoryQuery)({ after: "3", limit: "10" }))
expect(query).toEqual({ after: 3, limit: 10 })
})
})