Merge remote-tracking branch 'origin/v2' into search-integration

# Conflicts:
#	packages/client/test/promise.test.ts
#	packages/core/schema.json
#	packages/core/src/database/migration.gen.ts
#	packages/core/src/tool/websearch.ts
#	packages/sdk-next/src/index.ts
#	packages/sdk/js/src/v2/gen/types.gen.ts
This commit is contained in:
Shoubhit Dash 2026-07-07 17:38:46 +05:30
commit 7b8d8b8861
666 changed files with 46671 additions and 20220 deletions

View file

@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/server",
"version": "1.17.13",
"version": "1.17.14",
"private": true,
"type": "module",
"license": "MIT",

View file

@ -20,41 +20,36 @@ function eventData(data: unknown): Sse.Event {
export const EventHandler = HttpApiBuilder.group(Api, "server.event", (handlers) =>
Effect.gen(function* () {
const events = yield* EventV2.Service
return handlers
.handle(
"event.changes",
Effect.fn(() => Effect.succeed(events.changes())),
)
.handleRaw("event.subscribe", () =>
Effect.gen(function* () {
const connected = {
id: EventV2.ID.create(),
type: "server.connected",
data: {},
}
const output = Stream.unwrap(
Effect.gen(function* () {
// Acquiring the bounded stream installs its listener before readiness is observable.
const live = yield* EventV2.liveBounded(events, {
capacity: subscriberCapacity,
accept: isOpenCodeEvent,
})
return Stream.make(connected).pipe(Stream.concat(live))
}),
).pipe(Stream.map(eventData), Stream.pipeThroughChannel(Sse.encode()))
const heartbeat = Stream.tick("15 seconds").pipe(Stream.map(() => ": heartbeat\n\n"))
return HttpServerResponse.stream(
output.pipe(Stream.merge(heartbeat, { haltStrategy: "left" }), Stream.encodeText),
{
contentType: "text/event-stream",
headers: {
"Cache-Control": "no-cache, no-transform",
"X-Accel-Buffering": "no",
"X-Content-Type-Options": "nosniff",
},
return handlers.handleRaw("event.subscribe", () =>
Effect.gen(function* () {
const connected = {
id: EventV2.ID.create(),
type: "server.connected",
data: {},
}
const output = Stream.unwrap(
Effect.gen(function* () {
// Acquiring the bounded stream installs its listener before readiness is observable.
const live = yield* EventV2.liveBounded(events, {
capacity: subscriberCapacity,
accept: isOpenCodeEvent,
})
return Stream.make(connected).pipe(Stream.concat(live))
}),
).pipe(Stream.map(eventData), Stream.pipeThroughChannel(Sse.encode()))
const heartbeat = Stream.tick("15 seconds").pipe(Stream.map(() => ": heartbeat\n\n"))
return HttpServerResponse.stream(
output.pipe(Stream.merge(heartbeat, { haltStrategy: "left" }), Stream.encodeText),
{
contentType: "text/event-stream",
headers: {
"Cache-Control": "no-cache, no-transform",
"X-Accel-Buffering": "no",
"X-Content-Type-Options": "nosniff",
},
)
}),
)
},
)
}),
)
}),
)

View file

@ -38,10 +38,6 @@ export const MessageHandler = HttpApiBuilder.group(Api, "server.message", (handl
catch: () => new InvalidCursorError({ message: "Invalid cursor" }),
})
const order = decoded?.order ?? ctx.query.order ?? "desc"
// Read the watermark before the snapshot: an understated watermark only
// redelivers already-reflected events, while an overstated one would let
// an attached tail skip events missing from the snapshot.
const watermark = (yield* session.watermarks([ctx.params.sessionID])).get(ctx.params.sessionID)
const messages = yield* session
.messages({
sessionID: ctx.params.sessionID,
@ -74,7 +70,6 @@ export const MessageHandler = HttpApiBuilder.group(Api, "server.message", (handl
const last = messages.at(-1)
return {
data: messages,
watermark,
cursor: {
previous: first ? cursor.encode(first, order, "previous") : undefined,
next: last ? cursor.encode(last, order, "next") : undefined,

View file

@ -1,5 +1,5 @@
import { SessionV2 } from "@opencode-ai/core/session"
import { SessionContextEntry } from "@opencode-ai/core/session/context-entry"
import { InstructionEntry } from "@opencode-ai/core/session/instruction-entry"
import { DateTime, Effect, Stream } from "effect"
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
import { Api } from "../api"
@ -9,6 +9,7 @@ import {
CommandEvaluationError,
CommandNotFoundError,
InvalidCursorError,
InvalidRequestError,
MessageNotFoundError,
ServiceUnavailableError,
SessionBusyError,
@ -44,7 +45,6 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
const last = sessions.at(-1)
return {
data: sessions,
watermarks: Object.fromEntries(page.watermarks),
cursor: {
previous: first
? SessionsCursor.make({
@ -89,10 +89,8 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
"session.active",
Effect.fn(function* () {
const active = yield* session.active
const watermarks = yield* session.watermarks(Array.from(active))
return {
data: Object.fromEntries(Array.from(active, (sessionID) => [sessionID, { type: "running" as const }])),
watermarks: Object.fromEntries(watermarks),
}
}),
)
@ -113,6 +111,22 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
}
}),
)
.handle(
"session.remove",
Effect.fn(function* (ctx) {
yield* session.remove(ctx.params.sessionID).pipe(
Effect.catchTag(
"Session.NotFoundError",
(error) =>
new SessionNotFoundError({
sessionID: error.sessionID,
message: `Session not found: ${error.sessionID}`,
}),
),
)
return HttpApiSchema.NoContent.make()
}),
)
.handle(
"session.fork",
Effect.fn(function* (ctx) {
@ -216,6 +230,9 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
}),
),
),
Effect.catchTag("Session.AttachmentError", (error) =>
Effect.fail(new InvalidRequestError({ message: error.message, field: "prompt.files" })),
),
),
}
}),
@ -270,6 +287,9 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
}),
),
),
Effect.catchTag("Session.AttachmentError", (error) =>
Effect.fail(new InvalidRequestError({ message: error.message, field: "files" })),
),
),
}
}),
@ -344,44 +364,26 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
.handle(
"session.compact",
Effect.fn(function* (ctx) {
yield* session.compact({ sessionID: ctx.params.sessionID }).pipe(
Effect.catchTag("Session.NotFoundError", (error) =>
Effect.fail(
new SessionNotFoundError({
sessionID: error.sessionID,
message: `Session not found: ${error.sessionID}`,
}),
),
),
Effect.catchTag("Session.OperationUnavailableError", (error) =>
Effect.fail(
new ServiceUnavailableError({
message: `Session ${error.operation} is not available yet`,
service: `session.${error.operation}`,
}),
),
),
Effect.catchTag(
"Session.BusyError",
(error) =>
new SessionBusyError({
sessionID: error.sessionID,
message: `Session is busy: ${error.sessionID}`,
}),
),
Effect.catchTag("Session.MessageDecodeError", (error) => {
const ref = `err_${crypto.randomUUID().slice(0, 8)}`
return Effect.logError("failed to decode session message during compaction").pipe(
Effect.annotateLogs({ ref, sessionID: error.sessionID, messageID: error.messageID }),
Effect.andThen(
Effect.fail(
new UnknownError({ message: "Unexpected server error. Check server logs for details.", ref }),
),
return {
data: yield* session.compact({ sessionID: ctx.params.sessionID, id: ctx.payload.id }).pipe(
Effect.catchTag("Session.NotFoundError", (error) =>
Effect.fail(
new SessionNotFoundError({
sessionID: error.sessionID,
message: `Session not found: ${error.sessionID}`,
}),
),
)
}),
)
return HttpApiSchema.NoContent.make()
),
Effect.catchTag("Session.CompactionConflictError", (error) =>
Effect.fail(
new ConflictError({
message: `Compaction input ID conflicts with an existing durable record: ${error.inputID}`,
resource: error.inputID,
}),
),
),
),
}
}),
)
.handle(
@ -544,25 +546,25 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
}),
)
.handle(
"session.context.entry.list",
"session.instructions.entry.list",
Effect.fn(function* (ctx) {
const contextEntries = yield* SessionContextEntry.Service
return { data: yield* contextEntries.list(ctx.params.sessionID) }
const instructions = yield* InstructionEntry.Service
return { data: yield* instructions.list(ctx.params.sessionID) }
}),
)
.handle(
"session.context.entry.put",
"session.instructions.entry.put",
Effect.fn(function* (ctx) {
const contextEntries = yield* SessionContextEntry.Service
yield* contextEntries.put({ sessionID: ctx.params.sessionID, key: ctx.params.key, value: ctx.payload.value })
const instructions = yield* InstructionEntry.Service
yield* instructions.put({ sessionID: ctx.params.sessionID, key: ctx.params.key, value: ctx.payload.value })
return HttpApiSchema.NoContent.make()
}),
)
.handle(
"session.context.entry.remove",
"session.instructions.entry.remove",
Effect.fn(function* (ctx) {
const contextEntries = yield* SessionContextEntry.Service
yield* contextEntries.remove({ sessionID: ctx.params.sessionID, key: ctx.params.key })
const instructions = yield* InstructionEntry.Service
yield* instructions.remove({ sessionID: ctx.params.sessionID, key: ctx.params.key })
return HttpApiSchema.NoContent.make()
}),
)

View file

@ -38,6 +38,20 @@ export const ShellHandler = HttpApiBuilder.group(Api, "server.shell", (handlers)
)
}),
)
.handle(
"shell.timeout",
Effect.fn(function* (ctx) {
const shell = yield* Shell.Service
return yield* response(
shell.timeout(ctx.params.id, ctx.payload.timeout).pipe(
Effect.catchTag(
"Shell.NotFoundError",
() => new ShellNotFoundError({ id: ctx.params.id, message: `Shell command not found: ${ctx.params.id}` }),
),
),
)
}),
)
.handle(
"shell.output",
Effect.fn(function* (ctx) {

View file

@ -70,15 +70,17 @@ function makeRoutes<AuthError, AuthServices>(
[PluginRuntime.providerNode, PluginRuntime.providerNodeWithCell(pluginRuntimeCell)],
...(sdkPlugins ? [[SdkPlugins.node, SdkPlugins.layerWithStore(sdkPlugins)] as const] : []),
]
// Simulation replacements are loaded via dynamic import so the simulation
// module is never eagerly loaded. Layer.unwrap defers both the import and
// the app-node build to layer-build time; when simulation is off the branch
// is byte-for-byte identical to a plain AppNodeBuilder.build call.
const serviceLayer = simulationEnabled()
const serviceLayer = simulateEnabled()
? Layer.unwrap(
Effect.gen(function* () {
const { simulationReplacements } = yield* Effect.promise(() => import("@opencode-ai/simulation/backend"))
return AppNodeBuilder.build(applicationServices, [...replacements, ...simulationReplacements])
const { simulationReplacements, startDriveServer } = yield* Effect.promise(() =>
import("@opencode-ai/simulation/backend"),
)
if (driveEnabled()) startDriveServer()
return AppNodeBuilder.build(applicationServices, [
...replacements,
...(simulateEnabled() ? simulationReplacements : []),
])
}),
)
: AppNodeBuilder.build(applicationServices, replacements)
@ -96,8 +98,12 @@ function makeRoutes<AuthError, AuthServices>(
)
}
function simulationEnabled() {
return !!process.env.OPENCODE_SIMULATION
function simulateEnabled() {
return !!process.env.OPENCODE_SIMULATE
}
function driveEnabled() {
return !!process.env.OPENCODE_DRIVE
}
export const routes = createRoutes()