diff --git a/packages/client/src/effect/api/api.ts b/packages/client/src/effect/api/api.ts index 38758b0a7997..3c3961b32a2f 100644 --- a/packages/client/src/effect/api/api.ts +++ b/packages/client/src/effect/api/api.ts @@ -518,6 +518,7 @@ export type SessionLogOutput = readonly sessionID: Session.ID readonly parentID: Session.ID readonly boundary: Session.ForkBoundary + readonly messages?: ReadonlyArray | undefined readonly instructions?: | { readonly [x: string & Brand.Brand<"Instruction.Key">]: string & Brand.Brand<"Instruction.Hash"> } | undefined diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 7f8c229e9ff8..47e981a62d21 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -159,6 +159,76 @@ export type InstructionEntryKey = string export type SessionGenerateResponse = { data: { text: string } } +export type SessionMessageAgentSelected1 = { + id: string + metadata?: { [x: string]: any } + time: { created: number } + type: "agent-switched" + agent: string + previous?: string +} + +export type SessionMessageSynthetic1 = { + id: string + metadata?: { [x: string]: any } + time: { created: number } + text: string + description?: string + type: "synthetic" +} + +export type SessionMessageSystem1 = { + id: string + metadata?: { [x: string]: any } + time: { created: number } + type: "system" + text: string + description?: string +} + +export type SessionMessageSkill1 = { + id: string + metadata?: { [x: string]: any } + time: { created: number } + type: "skill" + skill: string + name: string + text: string +} + +export type SessionMessageShell1 = { + id: string + metadata?: { [x: string]: any } + time: { created: number; completed?: number } + type: "shell" + shellID: string + command: string + status: "running" | "exited" | "timeout" | "killed" + exit?: number + output?: { output: string; cursor: number; size: number; truncated: boolean } +} + +export type SessionMessageProviderState1 = { [x: string]: any } + +export type SessionMessageToolStateRunning1 = { + status: "running" + input: { [x: string]: any } + metadata: { [x: string]: JsonValue } +} + +export type ToolFileContent1 = { type: "file"; uri: string; mime: string; name?: string | undefined } + +export type SessionMessageCompactionRunning1 = { + type: "compaction" + id: string + metadata?: { [x: string]: any } + time: { created: number } + status: "running" + reason: "auto" | "manual" + summary: string + recent: string +} + export type SessionInboxSyntheticPayload1 = { text: string; description?: string; metadata?: { [x: string]: any } } export type ShellInfo = { @@ -174,16 +244,6 @@ export type ShellInfo = { time: { started: number; completed?: number } } -export type SessionMessageProviderState1 = { [x: string]: any } - -export type ToolFileContent1 = { type: "file"; uri: string; mime: string; name?: string | undefined } - -export type SessionMessageToolStateRunning1 = { - status: "running" - input: { [x: string]: any } - metadata: { [x: string]: JsonValue } -} - export type EventLogSynced = { type: "log.synced"; aggregateID: string; seq?: number } export type SessionInterruptResponse = { interrupted: boolean } @@ -454,6 +514,17 @@ export type SessionMessageLocationSwitched = { export type SessionInboxMovePayload = { location: LocationRef; projectID: string; subpath?: string } +export type SessionMessageLocationSwitched1 = { + id: string + metadata?: { [x: string]: any } + time: { created: number } + type: "location-switched" + location: LocationRef + projectID?: string + subpath?: string + previous?: { location: LocationRef; projectID?: string; subpath?: string } +} + export type V2EventRpc = { id: string created: number @@ -489,6 +560,15 @@ export type SessionMessageModelSelected = { previous?: ModelRef } +export type SessionMessageModelSelected1 = { + id: string + metadata?: { [x: string]: any } + time: { created: number } + type: "model-switched" + model: ModelRef + previous?: ModelRef +} + export type PromptFileAttachment = { data: PromptBase64 mime: string @@ -525,6 +605,16 @@ export type SessionMessageCompactionFailed = { error: SessionStructuredError } +export type SessionMessageCompactionFailed1 = { + type: "compaction" + id: string + metadata?: { [x: string]: any } + time: { created: number } + status: "failed" + reason: "auto" | "manual" + error: SessionStructuredError +} + export type SessionProviderContext = { version: 1; provenance: SessionProviderContextProvenance; messages: JsonValue } export type SessionInboxSynthetic = { @@ -1200,37 +1290,13 @@ export type McpResourcesChanged = { data: { server: string } } -export type SessionShellStarted = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.shell.started" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { sessionID: string; shell: ShellInfo } -} - -export type SessionShellEnded = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.shell.ended" - durable: { aggregateID: string; seq: number; version: 1 } - location?: LocationRef - data: { - sessionID: string - shell: ShellInfo - output: { output: string; cursor: number; size: number; truncated: boolean } - } -} +export type SessionMessageAssistantText1 = { type: "text"; text: string; state?: SessionMessageProviderState1 } -export type ShellCreated = { - id: string - created: number - metadata?: { [x: string]: any } - type: "shell.created" - location?: LocationRef - data: { info: ShellInfo } +export type SessionMessageAssistantReasoning1 = { + type: "reasoning" + text: string + state?: SessionMessageProviderState1 + time?: { created: number; completed?: number } } export type SessionStepEnded = { @@ -1333,16 +1399,40 @@ export type SessionToolCalled = { } } -export type SessionMessageAssistantText1 = { type: "text"; text: string; state?: SessionMessageProviderState1 } +export type ToolContent1 = ToolTextContent | ToolFileContent1 -export type SessionMessageAssistantReasoning1 = { - type: "reasoning" - text: string - state?: SessionMessageProviderState1 - time?: { created: number; completed?: number } +export type SessionShellStarted = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.shell.started" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { sessionID: string; shell: ShellInfo } } -export type ToolContent1 = ToolTextContent | ToolFileContent1 +export type SessionShellEnded = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.shell.ended" + durable: { aggregateID: string; seq: number; version: 1 } + location?: LocationRef + data: { + sessionID: string + shell: ShellInfo + output: { output: string; cursor: number; size: number; truncated: boolean } + } +} + +export type ShellCreated = { + id: string + created: number + metadata?: { [x: string]: any } + type: "shell.created" + location?: LocationRef + data: { info: ShellInfo } +} export type ModelCompatibility = { reasoningField?: ModelReasoningField @@ -1703,6 +1793,17 @@ export type SessionInboxUserPayload = { metadata?: { [x: string]: JsonValue } } +export type SessionMessageUser1 = { + id: string + metadata?: { [x: string]: any } + time: { created: number } + text: string + files?: Array + agents?: Array + skills?: Array + type: "user" +} + export type SessionInboxUserPayload1 = { text: string files?: Array @@ -1740,6 +1841,20 @@ export type SessionMessageCompactionCompleted = { providerContext?: SessionProviderContext } +export type SessionMessageCompactionCompleted1 = { + type: "compaction" + id: string + metadata?: { [x: string]: any } + time: { created: number } + status: "completed" + reason: "auto" | "manual" + model?: ModelRef + providerState?: SessionMessageProviderState1 + summary: string + recent: string + providerContext?: SessionProviderContext +} + export type SessionCompactionEnded = { id: string created: number @@ -1758,20 +1873,19 @@ export type SessionCompactionEnded = { } } -export type SessionForked = { - id: string - created: number - metadata?: { [x: string]: any } - type: "session.forked" - durable: { aggregateID: string; seq: number; version: 2 } - location?: LocationRef - data: { - sessionID: string - parentID: string - boundary: SessionForkBoundary - instructions?: { [x: string]: string } - instructionEntries?: InstructionEntrySnapshot - } +export type SessionMessageToolStateCompleted1 = { + status: "completed" + input: { [x: string]: any } + content: [ToolContent1, ...Array] + metadata?: { [x: string]: JsonValue } +} + +export type SessionMessageToolStateError1 = { + status: "error" + input: { [x: string]: any } + error: SessionStructuredError + content?: [ToolContent1, ...Array] + metadata?: { [x: string]: JsonValue } } export type SessionToolSuccess = { @@ -1811,21 +1925,6 @@ export type SessionToolFailed = { } } -export type SessionMessageToolStateCompleted1 = { - status: "completed" - input: { [x: string]: any } - content: [ToolContent1, ...Array] - metadata?: { [x: string]: JsonValue } -} - -export type SessionMessageToolStateError1 = { - status: "error" - input: { [x: string]: any } - error: SessionStructuredError - content?: [ToolContent1, ...Array] - metadata?: { [x: string]: JsonValue } -} - export type ModelInfo = { id: string modelID: string @@ -2104,6 +2203,11 @@ export type SessionMessageCompaction = | SessionMessageCompactionCompleted | SessionMessageCompactionFailed +export type SessionMessageCompaction1 = + | SessionMessageCompactionRunning1 + | SessionMessageCompactionCompleted1 + | SessionMessageCompactionFailed1 + export type SessionMessageAssistantTool1 = { type: "tool" id: string @@ -2153,6 +2257,24 @@ export type SessionMessageAssistant = { retry?: SessionMessageAssistantRetry } +export type SessionMessageAssistant1 = { + id: string + metadata?: { [x: string]: any } + time: { created: number; streamed?: number; completed?: number } + type: "assistant" + agent: string + model: ModelRef + content: Array + snapshot?: { start?: string; end?: string; files?: Array } + finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown" + rawFinish?: string + providerState?: SessionMessageProviderState1 + cost?: MoneyUSD + tokens?: TokenUsageInfo + error?: SessionStructuredError + retry?: SessionMessageAssistantRetry +} + export type SessionMessageAssistantContentEncoded = | SessionMessageAssistantText1 | SessionMessageAssistantReasoning1 @@ -2178,6 +2300,18 @@ export type SessionMessageInfo = | SessionMessageAssistant | SessionMessageCompaction +export type SessionMessageInfoEncoded = + | SessionMessageAgentSelected1 + | SessionMessageModelSelected1 + | SessionMessageLocationSwitched1 + | SessionMessageUser1 + | SessionMessageSynthetic1 + | SessionMessageSystem1 + | SessionMessageSkill1 + | SessionMessageShell1 + | SessionMessageAssistant1 + | SessionMessageCompaction1 + export type SessionMessageContentUpdated = { id: string created: number @@ -2210,6 +2344,31 @@ export type SessionMessagesResponse = { cursor: { previous?: string | null; next?: string | null } } +export type SessionForked = { + id: string + created: number + metadata?: { [x: string]: any } + type: "session.forked" + durable: { aggregateID: string; seq: number; version: 2 } + location?: LocationRef + data: { + sessionID: string + parentID: string + boundary: SessionForkBoundary + messages?: Array + instructions?: { [x: string]: string } + instructionEntries?: InstructionEntrySnapshot + } +} + +export type IntegrationInfo = { + id: string + name: string + metadata?: { [x: string]: any } + methods: Array + connections: Array +} + export type SessionEventDurable = | SessionCreated | SessionAgentSelected @@ -2255,14 +2414,6 @@ export type SessionEventDurable = | SessionMessageContentUpdated | SessionUsageRecorded -export type IntegrationInfo = { - id: string - name: string - metadata?: { [x: string]: any } - methods: Array - connections: Array -} - export type V2Event = | ModelsDevRefreshed | CredentialUpdated diff --git a/packages/core/src/plugin/host.ts b/packages/core/src/plugin/host.ts index 44beadcc6ccf..271413edb0d7 100644 --- a/packages/core/src/plugin/host.ts +++ b/packages/core/src/plugin/host.ts @@ -513,6 +513,7 @@ export const make = Effect.fn("PluginHost.make")(function* ( input?.location ?? Location.Ref.make({ directory: location.directory, workspaceID: location.workspaceID }), }), get: (input) => sessions.get(input.sessionID), + fork: sessions.fork, switchAgent: sessions.switchAgent, switchModel: sessions.switchModel, prompt: sessions.prompt, diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 04429fd23157..f1038c75ccf4 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -4,7 +4,7 @@ export * from "./session/schema.js" import { Effect, Layer, Schema, Context, Stream } from "effect" import { LLMClient } from "@opencode/ai" import { ListAnchor } from "@opencode/schema/session" -import { and, desc, eq } from "drizzle-orm" +import { and, asc, desc, eq, lt, lte, sql } from "drizzle-orm" import { Project } from "./project.js" import { Model } from "@opencode/schema/model" import { Location } from "./location.js" @@ -23,6 +23,7 @@ import { Slug } from "./util/slug.js" import path from "path" import { SessionRunner } from "./session/runner/index.js" import { SessionStore } from "./session/store.js" +import { decodeMessageRow } from "./session/history.js" import { SessionExecution } from "./session/execution.js" import { AttachmentError, @@ -93,6 +94,7 @@ type CompactInput = Parameters[0] & { sessionID: Sess type ForkInput = { sessionID: SessionSchema.ID boundary: SessionSchema.ForkRequestBoundary + filter?: (messages: readonly SessionMessage.Info[]) => readonly SessionMessage.Info[] } export { @@ -293,7 +295,7 @@ const layer = Layer.effect( fork: Effect.fn("Session.fork")(function* (input) { const parent = yield* result.get(input.sessionID) const boundary = yield* db - .select({ id: SessionMessageTable.id }) + .select({ id: SessionMessageTable.id, seq: SessionMessageTable.seq }) .from(SessionMessageTable) .where( and( @@ -311,6 +313,31 @@ const layer = Layer.effect( messageID: input.boundary.messageID, }) if (!boundary) return yield* new ForkEmptyError({ sessionID: input.sessionID }) + const messages = input.filter + ? input.filter( + yield* db + .select() + .from(SessionMessageTable) + .where( + and( + eq(SessionMessageTable.session_id, parent.id), + input.boundary.type === "before" + ? lt(SessionMessageTable.seq, boundary.seq) + : lte(SessionMessageTable.seq, boundary.seq), + sql`${SessionMessageTable.type} != 'assistant' or json_extract(${SessionMessageTable.data}, '$.time.completed') is not null`, + sql`${SessionMessageTable.type} != 'shell' or json_extract(${SessionMessageTable.data}, '$.status') != 'running'`, + sql`${SessionMessageTable.type} != 'compaction' or json_extract(${SessionMessageTable.data}, '$.status') != 'running'`, + ), + ) + .orderBy(asc(SessionMessageTable.seq)) + .all() + .pipe( + Effect.orDie, + Effect.flatMap((rows) => Effect.forEach(rows, decodeMessageRow)), + Effect.orDie, + ), + ) + : undefined const sessionID = SessionSchema.ID.create() const inherited = yield* db .transaction(() => @@ -327,6 +354,7 @@ const layer = Layer.effect( sessionID, parentID: parent.id, boundary: { ...input.boundary, messageID: boundary.id }, + messages: messages === undefined ? undefined : Schema.encodeSync(Schema.Array(SessionMessage.Info))(messages), ...inherited, }) return yield* result.get(sessionID).pipe(Effect.orDie) diff --git a/packages/core/src/session/projector.ts b/packages/core/src/session/projector.ts index dbc9d90950da..37f4db8053eb 100644 --- a/packages/core/src/session/projector.ts +++ b/packages/core/src/session/projector.ts @@ -179,6 +179,32 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* ( if (event.data.instructionEntries) yield* InstructionEntry.initialize(db, event.data.sessionID, event.data.instructionEntries, event.created) + if (event.data.messages !== undefined) { + if (event.data.messages.length > 0) { + yield* db + .insert(SessionMessageTable) + .values( + event.data.messages.map((message, index) => { + const { id: _, type, ...data } = message + return { + id: SessionMessage.ID.make(`${SessionMessage.ID.fromEvent(event.id)}_${index + 1}`), + session_id: event.data.sessionID, + type, + seq: index + 1, + time_created: data.time.created, + data, + } + }), + ) + .run() + .pipe(Effect.orDie) + yield* Bus.reserveSequence(db, event.data.sessionID, event.data.messages.length) + } + if (event.data.instructions) + yield* InstructionState.initialize(db, event.data.sessionID, event.durable.seq, event.data.instructions) + return + } + let cursor = -1 while (copiedSeq !== undefined) { const rows = yield* db diff --git a/packages/core/test/plugin/host.ts b/packages/core/test/plugin/host.ts index 3cbe696ab4e8..97406479c649 100644 --- a/packages/core/test/plugin/host.ts +++ b/packages/core/test/plugin/host.ts @@ -162,6 +162,7 @@ export function host(overrides: Overrides = {}): Plugin.Context { session: { hook: overrides.session?.hook ?? (() => Effect.die("unused session.hook")), create: overrides.session?.create ?? (() => Effect.die("unused session.create")), + fork: overrides.session?.fork ?? (() => Effect.die("unused session.fork")), get: overrides.session?.get ?? (() => Effect.die("unused session.get")), switchAgent: overrides.session?.switchAgent ?? (() => Effect.die("unused session.switchAgent")), switchModel: overrides.session?.switchModel ?? (() => Effect.die("unused session.switchModel")), diff --git a/packages/core/test/session-create.test.ts b/packages/core/test/session-create.test.ts index dc2f31ee5eab..fb6e418dbe4f 100644 --- a/packages/core/test/session-create.test.ts +++ b/packages/core/test/session-create.test.ts @@ -40,6 +40,9 @@ import { offlineModels } from "./fixture/models" import { promptLocationNode } from "./fixture/prompt-location" import { globalProjectNode } from "./lib/project" import { tmpdirScoped } from "./fixture/tmpdir" +import { Plugin } from "@opencode/plugin" +import { PluginPromise } from "@opencode/core/plugin/promise" +import { host } from "./plugin/host" const it = testEffect( AppNodeBuilder.build( @@ -561,6 +564,98 @@ describe("Session.create", () => { }), ) + it.effect("freezes filtered fork history and replays it without invoking the callback", () => + Effect.gen(function* () { + const session = yield* Session.Service + const bus = yield* Bus.Service + const { db } = yield* Database.Service + const parent = yield* session.create({ location }) + yield* session.prompt({ sessionID: parent.id, text: "First", resume: false }) + yield* SessionInbox.promote(db, bus, parent.id, "steer") + yield* session.synthetic({ sessionID: parent.id, text: "Original note", resume: false }) + yield* SessionInbox.promote(db, bus, parent.id, "steer") + const last = yield* session.prompt({ sessionID: parent.id, text: "Excluded by boundary", resume: false }) + yield* SessionInbox.promote(db, bus, parent.id, "steer") + const calls: number[] = [] + const forked = yield* session.fork({ + sessionID: parent.id, + boundary: { type: "before", messageID: last.id }, + filter: (messages) => { + calls.push(messages.length) + return messages.flatMap((message) => + message.type === "synthetic" ? [{ ...message, text: "Filtered note" }] : [], + ) + }, + }) + const original = yield* session.context(forked.id) + expect(calls).toEqual([2]) + expect(original).toMatchObject([{ type: "synthetic", text: "Filtered note" }]) + const event = Array.from(yield* Stream.runCollect(logEvents(session, forked.id)))[0] + if (event.type !== "session.forked") return yield* Effect.die(new Error("Fork event not found")) + expect(typeof event.data.messages?.[0].time.created).toBe("number") + expect((yield* session.context(parent.id))[1]).toMatchObject({ text: "Original note" }) + const recorded = yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, forked.id)).get() + if (!recorded) return yield* Effect.die(new Error("Fork event not found")) + yield* bus.remove(forked.id) + yield* db.delete(SessionTable).where(eq(SessionTable.id, forked.id)).run() + yield* bus.replay({ + id: recorded.id, + created: recorded.created, + aggregateID: recorded.aggregate_id, + seq: recorded.seq, + type: recorded.type, + data: recorded.data, + }) + expect(yield* session.context(forked.id)).toEqual(original) + expect(calls).toEqual([2]) + yield* session.prompt({ sessionID: forked.id, text: "Continue", resume: false }) + yield* SessionInbox.promote(db, bus, forked.id, "steer") + expect(yield* session.context(forked.id)).toMatchObject([ + { type: "synthetic", text: "Filtered note" }, + { type: "user", text: "Continue" }, + ]) + const empty = yield* session.fork({ + sessionID: parent.id, + boundary: { type: "through" }, + filter: () => [], + }) + expect(yield* session.context(empty.id)).toEqual([]) + }), + ) + + it.effect("exposes filtered forks to Promise plugins with decoded callback messages", () => + Effect.gen(function* () { + const session = yield* Session.Service + const bus = yield* Bus.Service + const { db } = yield* Database.Service + const parent = yield* session.create({ location }) + yield* session.prompt({ sessionID: parent.id, text: "Keep", resume: false }) + yield* SessionInbox.promote(db, bus, parent.id, "steer") + yield* session.synthetic({ sessionID: parent.id, text: "Drop", resume: false }) + yield* SessionInbox.promote(db, bus, parent.id, "steer") + const plugin = PluginPromise.fromPromise( + Plugin.define({ + id: "filtered-fork", + async setup(ctx) { + const fork = await ctx.session.fork({ + sessionID: parent.id, + boundary: { type: "through" }, + filter: (messages) => { + expect(DateTime.isDateTime(messages[0].time.created)).toBe(true) + return messages.filter((message) => message.type === "user") + }, + }) + expect(typeof fork.time.created).toBe("number") + expect(await ctx.session.context({ sessionID: fork.id })).toMatchObject([{ type: "user", text: "Keep" }]) + }, + }), + ) + yield* plugin.effect( + host({ session: { fork: session.fork, context: (input) => session.context(input.sessionID) } }), + ) + }), + ) + it.effect("keeps a fork untitled when its parent is untitled", () => Effect.gen(function* () { const session = yield* Session.Service diff --git a/packages/plugin/src/effect/session.ts b/packages/plugin/src/effect/session.ts index 89cb42bc7c4e..d8f96ded903d 100644 --- a/packages/plugin/src/effect/session.ts +++ b/packages/plugin/src/effect/session.ts @@ -98,5 +98,11 @@ export type SessionDomain = Pick< | "wait" | "context" > & { + readonly fork: ( + input: Parameters["fork"]>[0] & { + /** Select or transform settled history in chronological order. Runs once before the fork is recorded. */ + readonly filter?: (messages: readonly SessionMessage.Info[]) => readonly SessionMessage.Info[] + }, + ) => ReturnType["fork"]> readonly hook: ModelHooks } diff --git a/packages/plugin/src/promise/adapter.ts b/packages/plugin/src/promise/adapter.ts index 8e09b61e1e81..549191aa8e00 100644 --- a/packages/plugin/src/promise/adapter.ts +++ b/packages/plugin/src/promise/adapter.ts @@ -566,6 +566,12 @@ export function fromPromise(plugin: Plugin) { host.session.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))), options), ), create: adaptApiMethod(SessionEndpoints["session.create"], host.session.create), + fork: (input) => + adaptApiMethod( + SessionEndpoints["session.fork"], + (request: Parameters[0]) => + host.session.fork({ ...request, filter: input.filter }), + )(input), get: adaptApiMethod(SessionEndpoints["session.get"], host.session.get), switchAgent: adaptApiMethod(SessionEndpoints["session.switchAgent"], host.session.switchAgent), switchModel: adaptApiMethod(SessionEndpoints["session.switchModel"], host.session.switchModel), diff --git a/packages/plugin/src/promise/session.ts b/packages/plugin/src/promise/session.ts index 2a89c007d587..3fbfc4f46c52 100644 --- a/packages/plugin/src/promise/session.ts +++ b/packages/plugin/src/promise/session.ts @@ -98,5 +98,11 @@ export type SessionDomain = Pick< | "wait" | "context" > & { + readonly fork: ( + input: Parameters[0] & { + /** Select or transform settled history in chronological order. Uses decoded Session messages, like session hooks. */ + readonly filter?: (messages: readonly SessionMessage.Info[]) => readonly SessionMessage.Info[] + }, + ) => ReturnType readonly hook: ModelHooks } diff --git a/packages/protocol/openapi.json b/packages/protocol/openapi.json index 2b36ba43a68f..a47a35151dbe 100644 --- a/packages/protocol/openapi.json +++ b/packages/protocol/openapi.json @@ -1621,14 +1621,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - } - ] + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" } } } diff --git a/packages/schema/src/session-event.ts b/packages/schema/src/session-event.ts index d82ad08a0aa1..9b0094849f65 100644 --- a/packages/schema/src/session-event.ts +++ b/packages/schema/src/session-event.ts @@ -174,6 +174,8 @@ export const Forked = Event.durable({ ...Base, parentID: SessionID, boundary: SessionFork.Boundary, + /** Frozen plugin-selected history. Omitted for ordinary boundary-based forks. */ + messages: Schema.Array(SessionMessage.InfoEncoded).pipe(optional), instructions: Instruction.Values.pipe(optional), instructionEntries: InstructionEntry.Snapshot.pipe(optional), }, diff --git a/packages/schema/src/session-message.ts b/packages/schema/src/session-message.ts index 6bd37eb51746..2f4dc21d7ebb 100644 --- a/packages/schema/src/session-message.ts +++ b/packages/schema/src/session-message.ts @@ -296,3 +296,6 @@ export type Info = | Assistant | Compaction export type Type = Info["type"] + +export const InfoEncoded = Schema.toEncoded(Info).annotate({ identifier: "Session.Message.Info.Encoded" }) +export type InfoEncoded = typeof InfoEncoded.Type diff --git a/services/www/openapi.json b/services/www/openapi.json index 2b36ba43a68f..a47a35151dbe 100644 --- a/services/www/openapi.json +++ b/services/www/openapi.json @@ -1621,14 +1621,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - } - ] + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" } } } diff --git a/services/www/public/openapi.json b/services/www/public/openapi.json index 2b36ba43a68f..a47a35151dbe 100644 --- a/services/www/public/openapi.json +++ b/services/www/public/openapi.json @@ -1621,14 +1621,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - } - ] + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" } } }