diff --git a/CONTEXT.md b/CONTEXT.md index 97919b63f6..d379facbba 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -141,6 +141,7 @@ _Avoid_: Response envelope - Promise streaming methods return a lazy `AsyncIterable` directly rather than a Promise-wrapped stream object. Iteration opens the connection, `AbortSignal` cancels it, and ending iteration closes the underlying request; the Effect emitter analogously returns `Stream` directly. - Promise SSE connection establishment, declared HTTP failures, and infrastructure failures occur during `AsyncIterable` iteration, beginning with its first `next()` call, rather than during synchronous method construction. - Neither generated streaming runtime automatically reconnects after disconnection. Promise `AsyncIterable` and Effect `Stream` fail explicitly; live consumers refresh and resubscribe, while durable sequence-based resume remains explicit composition above the generated client. +- Event definition durability is authoritative for published payloads. Durable definitions publish and decode only with commit metadata (`aggregateID`, `seq`, and `version`); live definitions forbid that metadata. Core's pre-commit payload without an assigned sequence is a separate internal type and never reaches subscribers or projectors. - Promise client construction is synchronous and network-free. It requires `baseUrl`, defaults to `globalThis.fetch`, accepts client-level headers, and merges them with per-call header overrides. - Effect client construction accepts an explicit `baseUrl` and obtains `HttpClient.HttpClient` from the Effect environment. It does not install fetch or duplicate per-call transport policy; callers transform/provide the client for headers, tracing, retries, recording, and tests, while fiber interruption owns cancellation. - Promise and Effect emitters each own their generated public type modules. The **SDK Contract IR**, not a physically shared generated type package, is the common source; this permits zero-Effect wire types and rich decoded Effect types to evolve independently. diff --git a/packages/core/src/event.ts b/packages/core/src/event.ts index 132a88b111..2c0261ebbf 100644 --- a/packages/core/src/event.ts +++ b/packages/core/src/event.ts @@ -2,7 +2,14 @@ export * as EventV2 from "./event" import { Cause, Context, Effect, Layer, Option, PubSub, Schema, Stream } from "effect" import { Event } from "@opencode-ai/schema/event" -import type { Data, Definition, Payload } from "@opencode-ai/schema/event" +import type { + Data, + Definition, + DurableDefinition, + LiveDefinition, + Payload, + UncommittedPayload, +} from "@opencode-ai/schema/event" import { and, asc, eq, gt } from "drizzle-orm" import { Database } from "./database/database" import { EventSequenceTable, EventTable } from "./event/sql" @@ -124,8 +131,25 @@ export const layerWith = (options?: LayerOptions) => ) function commitDurableEvent( - definition: Definition, - event: Payload, + definition: DurableDefinition, + event: UncommittedPayload, + input: undefined, + commit?: (seq: number) => Effect.Effect, + ): Effect.Effect> + function commitDurableEvent( + definition: DurableDefinition, + event: UncommittedPayload, + input: { + readonly seq: number + readonly aggregateID: string + readonly ownerID?: string + readonly strictOwner?: boolean + }, + commit?: (seq: number) => Effect.Effect, + ): Effect.Effect | undefined> + function commitDurableEvent( + definition: DurableDefinition, + event: UncommittedPayload, input?: { readonly seq: number readonly aggregateID: string @@ -135,7 +159,7 @@ export const layerWith = (options?: LayerOptions) => commit?: (seq: number) => Effect.Effect, ) { return Effect.gen(function* () { - const durable = definition?.durable + const durable = definition.durable if (durable) { const aggregateID = (event.data as Record)[durable.aggregate] if (typeof aggregateID !== "string") { @@ -200,7 +224,7 @@ export const layerWith = (options?: LayerOptions) => .run() .pipe(Effect.orDie) } - return + return undefined } yield* Effect.die( new InvalidDurableEventError({ @@ -210,7 +234,7 @@ export const layerWith = (options?: LayerOptions) => ) } if (input && row?.ownerID && row.ownerID !== input.ownerID) { - return + return undefined } const seq = input?.seq ?? latest + 1 if (input && seq !== latest + 1) { @@ -234,10 +258,10 @@ export const layerWith = (options?: LayerOptions) => message: `Event ${event.id} already exists at aggregate ${stored.aggregateID} sequence ${stored.seq}`, }), ) - const committed = { + const committed: Payload = { ...event, durable: { aggregateID, seq, version: durable.version }, - } as Payload + } for (const projector of list) { yield* projector(committed) } @@ -267,14 +291,14 @@ export const layerWith = (options?: LayerOptions) => ]) .run() .pipe(Effect.orDie) - return { aggregateID, seq } + return committed }), { behavior: "immediate" }, ) .pipe(Effect.orDie) if (committed) { yield* Effect.forEach( - pubsub.durable.get(committed.aggregateID) ?? [], + pubsub.durable.get(committed.durable.aggregateID) ?? [], (wake) => PubSub.publish(wake, undefined), { discard: true }, ) @@ -287,35 +311,6 @@ export const layerWith = (options?: LayerOptions) => }) } - function publishEvent(definition: D, event: Payload, commit?: PublishOptions["commit"]) { - return Effect.gen(function* () { - if (!definition?.durable && commit) - return yield* Effect.die( - new InvalidDurableEventError({ - type: event.type, - message: "Local commit hooks require a durable event", - }), - ) - if (definition?.durable) { - const committed = yield* commitDurableEvent(definition, event as Payload, undefined, commit) - if (committed) { - event = { - ...event, - durable: { - aggregateID: committed.aggregateID, - seq: committed.seq, - version: definition.durable.version, - }, - } - yield* notify(event as Payload, true) - return event - } - } - yield* notify(event as Payload, false) - return event - }) - } - const observe = (event: Payload, observer: (event: Payload) => Effect.Effect) => Effect.suspend(() => observer(event)).pipe( Effect.catchCauseIf( @@ -337,7 +332,12 @@ export const layerWith = (options?: LayerOptions) => }) } - function publish(definition: D, data: Data, options?: PublishOptions) { + function publish( + definition: D, + data: Data, + options?: PublishOptions, + ): Effect.Effect> + function publish(definition: Definition, data: unknown, options?: PublishOptions): Effect.Effect { return Effect.gen(function* () { const serviceLocation = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service)) const location = @@ -345,17 +345,34 @@ export const layerWith = (options?: LayerOptions) => (serviceLocation ? { directory: serviceLocation.directory, workspaceID: serviceLocation.workspaceID } : undefined) - return yield* publishEvent( - definition, - { + if (definition.durable) { + const event: UncommittedPayload = { id: options?.id ?? ID.create(), ...(options?.metadata ? { metadata: options.metadata } : {}), type: definition.type, ...(location ? { location } : {}), data, - } as Payload, - options?.commit, - ) + } + const committed = yield* commitDurableEvent(definition, event, undefined, options?.commit) + yield* notify(committed, true) + return committed + } + if (options?.commit) + return yield* Effect.die( + new InvalidDurableEventError({ + type: definition.type, + message: "Local commit hooks require a durable event", + }), + ) + const event: Payload = { + id: options?.id ?? ID.create(), + ...(options?.metadata ? { metadata: options.metadata } : {}), + type: definition.type, + ...(location ? { location } : {}), + data, + } + yield* notify(event, false) + return event }) } @@ -370,11 +387,11 @@ export const layerWith = (options?: LayerOptions) => new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }), ) } else { - const payload = { + const payload: UncommittedPayload = { id: event.id, type: definition.type, data: Schema.decodeUnknownSync(definition.data)(event.data), - } as Payload + } const committed = yield* commitDurableEvent(definition, payload, { seq: event.seq, aggregateID: event.aggregateID, @@ -382,17 +399,7 @@ export const layerWith = (options?: LayerOptions) => strictOwner: options?.strictOwner, }) if (committed && options?.publish) { - yield* notify( - { - ...payload, - durable: { - aggregateID: committed.aggregateID, - seq: committed.seq, - version: definition.durable.version, - }, - }, - true, - ) + yield* notify(committed, true) } } }) @@ -459,7 +466,7 @@ export const layerWith = (options?: LayerOptions) => const streamAll = (): Stream.Stream => Stream.fromPubSub(pubsub.all) - const decodeSerializedEvent = (event: SerializedEvent) => { + const decodeSerializedEvent = (event: SerializedEvent): Payload => { const definition = Durable.get(event.type) if (!definition?.durable) { throw new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }) diff --git a/packages/core/test/event.test.ts b/packages/core/test/event.test.ts index e2b2a5df04..26d93b4d99 100644 --- a/packages/core/test/event.test.ts +++ b/packages/core/test/event.test.ts @@ -116,7 +116,7 @@ describe("EventV2", () => { const event = yield* events.publish(VersionedMessage, { id: "one", text: "hello" }) expect(event.type).toBe("test.versioned") - expect(event.durable?.version).toBe(2) + expect(event.durable.version).toBe(2) }), ) @@ -138,6 +138,50 @@ describe("EventV2", () => { }), ) + it.effect("preserves same-type projector routing across durable versions", () => + Effect.gen(function* () { + const events = yield* EventV2.Service + const historical = EventV2.define({ + type: "test.projector-version", + durable: { version: 1, aggregate: "id" }, + schema: { id: Schema.String }, + }) + const current = EventV2.define({ + type: "test.projector-version", + durable: { version: 2, aggregate: "id" }, + schema: { id: Schema.String }, + }) + const received = new Array() + yield* events.project(historical, (event) => Effect.sync(() => received.push(event))) + + const published = yield* events.publish(current, { id: "aggregate" }) + + expect(received).toEqual([published]) + }), + ) + + it.effect("preserves same-type subscription routing across durable versions", () => + Effect.gen(function* () { + const events = yield* EventV2.Service + const historical = EventV2.define({ + type: "test.subscription-version", + durable: { version: 1, aggregate: "id" }, + schema: { id: Schema.String }, + }) + const current = EventV2.define({ + type: "test.subscription-version", + durable: { version: 2, aggregate: "id" }, + schema: { id: Schema.String }, + }) + const fiber = yield* events.subscribe(historical).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) + yield* Effect.yieldNow + + const published = yield* events.publish(current, { id: "aggregate" }) + + expect(Array.from(yield* Fiber.join(fiber))).toEqual([published]) + }), + ) + it.effect("publishes to typed and wildcard subscriptions", () => Effect.gen(function* () { const events = yield* EventV2.Service @@ -764,7 +808,7 @@ describe("EventV2", () => { const replayed = { id: published.id, type: EventV2.versionedType(DurableMessage.type, 1), - seq: published.durable!.seq, + seq: published.durable.seq, aggregateID, data: published.data, } diff --git a/packages/core/test/session-runner-tool-events.test.ts b/packages/core/test/session-runner-tool-events.test.ts index f96ea4dea2..2fe34c6276 100644 --- a/packages/core/test/session-runner-tool-events.test.ts +++ b/packages/core/test/session-runner-tool-events.test.ts @@ -17,7 +17,14 @@ const capture = () => { const events = EventV2.Service.of({ publish: (definition, data) => Effect.sync(() => { - const event = { id: EventV2.ID.create(), type: definition.type, data } as EventV2.Payload + const event = { + id: EventV2.ID.create(), + type: definition.type, + ...(definition.durable + ? { durable: { aggregateID: sessionID, seq: published.length, version: definition.durable.version } } + : {}), + data, + } as EventV2.Payload published.push({ type: definition.durable ? EventV2.versionedType(definition.type, definition.durable.version) diff --git a/packages/opencode/test/server/httpapi-public-openapi.test.ts b/packages/opencode/test/server/httpapi-public-openapi.test.ts index a8f6f8d1c8..73a0158795 100644 --- a/packages/opencode/test/server/httpapi-public-openapi.test.ts +++ b/packages/opencode/test/server/httpapi-public-openapi.test.ts @@ -99,6 +99,17 @@ describe("PublicApi OpenAPI v2 errors", () => { }) }) + test("documents durable metadata only on durable events", () => { + const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec + const durable = spec.components.schemas.V2EventSessionCreated + const live = spec.components.schemas.V2EventSessionNextTextDelta + + expect(durable?.required).toContain("durable") + expect(durable?.properties?.durable).toBeDefined() + expect(live?.required).not.toContain("durable") + expect(Reflect.get(live?.properties?.durable ?? {}, "not")).toEqual({}) + }) + test("preserves /api auth responses", () => { const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec diff --git a/packages/opencode/test/server/httpapi-v2-location.test.ts b/packages/opencode/test/server/httpapi-v2-location.test.ts index e178062e7a..172e0bd4a1 100644 --- a/packages/opencode/test/server/httpapi-v2-location.test.ts +++ b/packages/opencode/test/server/httpapi-v2-location.test.ts @@ -23,6 +23,7 @@ function request(route: string, directory: string, init: RequestInit = {}) { const Event = Schema.Struct({ id: EventV2.ID, type: Schema.String, + durable: Schema.optional(Schema.Struct({ aggregateID: Schema.String, seq: Schema.Int, version: Schema.Int })), location: Schema.optional(Location.Ref), data: Schema.Unknown, }) @@ -81,12 +82,15 @@ describe("v2 location HttpApi", () => { const reader = response.body!.getReader() const connected = await readEvent(reader) expect(connected.type).toBe("server.connected") + expect(connected).not.toHaveProperty("durable") expect(connected.location).toBeUndefined() const created = await request("/session", publisher.path, { method: "POST" }) expect(created.status).toBe(200) + const session = (await created.json()) as { id: string } expect(await readEventType(reader, "session.created")).toMatchObject({ type: "session.created", + durable: { aggregateID: session.id, seq: 0, version: 1 }, location: { directory: publisher.path }, data: { sessionID: expect.any(String) }, }) diff --git a/packages/opencode/test/v2/session-message-updater.test.ts b/packages/opencode/test/v2/session-message-updater.test.ts index 668a353f67..56478a4d1d 100644 --- a/packages/opencode/test/v2/session-message-updater.test.ts +++ b/packages/opencode/test/v2/session-message-updater.test.ts @@ -9,6 +9,12 @@ import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionMessageUpdater } from "@opencode-ai/core/session/message-updater" import { SessionMessage } from "@opencode-ai/core/session/message" +function durable(sessionID: SessionID, seq?: number): { aggregateID: SessionID; seq: number; version: 1 } +function durable(sessionID: SessionID, seq: number, version: 2): { aggregateID: SessionID; seq: number; version: 2 } +function durable(sessionID: SessionID, seq = 0, version: 1 | 2 = 1) { + return { aggregateID: sessionID, seq, version } +} + test.skip("step snapshots carry over to assistant messages", () => { const state: SessionMessageUpdater.MemoryState = { messages: [] } const sessionID = SessionID.make("session") @@ -17,6 +23,7 @@ test.skip("step snapshots carry over to assistant messages", () => { Effect.runSync( SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { id: EventV2.ID.create(), + durable: durable(sessionID), type: "session.next.step.started", data: { sessionID, @@ -38,6 +45,7 @@ test.skip("step snapshots carry over to assistant messages", () => { Effect.runSync( SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { id: EventV2.ID.create(), + durable: durable(sessionID, 1, 2), type: "session.next.step.ended", data: { sessionID, @@ -70,6 +78,7 @@ test.skip("text ended populates assistant text content", () => { Effect.runSync( SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { id: EventV2.ID.create(), + durable: durable(sessionID), type: "session.next.step.started", data: { sessionID, @@ -88,6 +97,7 @@ test.skip("text ended populates assistant text content", () => { Effect.runSync( SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { id: EventV2.ID.create(), + durable: durable(sessionID, 1), type: "session.next.text.started", data: { sessionID, @@ -101,6 +111,7 @@ test.skip("text ended populates assistant text content", () => { Effect.runSync( SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { id: EventV2.ID.create(), + durable: durable(sessionID, 2), type: "session.next.text.ended", data: { sessionID, @@ -126,6 +137,7 @@ test.skip("tool completion stores completed timestamp", () => { Effect.runSync( SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { id: EventV2.ID.create(), + durable: durable(sessionID), type: "session.next.step.started", data: { sessionID, @@ -144,6 +156,7 @@ test.skip("tool completion stores completed timestamp", () => { Effect.runSync( SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { id: EventV2.ID.create(), + durable: durable(sessionID, 1), type: "session.next.tool.input.started", data: { sessionID, @@ -158,6 +171,7 @@ test.skip("tool completion stores completed timestamp", () => { Effect.runSync( SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { id: EventV2.ID.create(), + durable: durable(sessionID, 2), type: "session.next.tool.called", data: { sessionID, @@ -174,6 +188,7 @@ test.skip("tool completion stores completed timestamp", () => { Effect.runSync( SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { id: EventV2.ID.create(), + durable: durable(sessionID, 3), type: "session.next.tool.success", data: { sessionID, @@ -204,6 +219,7 @@ test("compaction events reduce to compaction message only when completed", () => Effect.runSync( SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { id, + durable: durable(sessionID), type: "session.next.compaction.started", data: { sessionID, @@ -245,6 +261,7 @@ test("compaction events reduce to compaction message only when completed", () => Effect.runSync( SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { id: EventV2.ID.create(), + durable: durable(sessionID, 3), type: "session.next.compaction.ended", data: { sessionID, diff --git a/packages/protocol/src/groups/event.ts b/packages/protocol/src/groups/event.ts index 862337e54b..cf6195a264 100644 --- a/packages/protocol/src/groups/event.ts +++ b/packages/protocol/src/groups/event.ts @@ -8,24 +8,34 @@ import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" const fields = { id: Event.ID, metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), - durable: Schema.optional(Schema.Struct({ aggregateID: Schema.String, seq: Schema.Int, version: Schema.Int })), location: Schema.optional(Location.Ref), } +const DurableEnvelope = Schema.Struct({ aggregateID: Schema.String, seq: Schema.Int, version: Schema.Int }) + const schema = (definitions: ReadonlyArray) => Schema.Union([ ...definitions.map((definition) => - Schema.Struct({ - ...fields, - type: Schema.Literal(definition.type), - data: definition.data, - }).annotate({ identifier: `V2Event.${definition.type}` }), + definition.durable + ? Schema.Struct({ + ...fields, + durable: DurableEnvelope, + type: Schema.Literal(definition.type), + data: definition.data, + }).annotate({ identifier: `V2Event.${definition.type}` }) + : Schema.Struct({ + ...fields, + durable: Schema.optional(Schema.Never), + type: Schema.Literal(definition.type), + data: definition.data, + }).annotate({ identifier: `V2Event.${definition.type}` }), ), ...(definitions.some((definition) => definition.type === "server.connected") ? [] : [ Schema.Struct({ ...fields, + durable: Schema.optional(Schema.Never), type: Schema.Literal("server.connected"), data: Schema.Struct({}), }).annotate({ identifier: "V2Event.server.connected" }), @@ -56,4 +66,5 @@ export const makeEventGroup = (definitions: ReadonlyArray) => make(d const event = make(EventManifest.ServerDefinitions) export const EventGroup = event.group -export type Event = typeof event.schema.Type +export const EventSchema = event.schema +export type Event = typeof EventSchema.Type diff --git a/packages/protocol/test/event.test.ts b/packages/protocol/test/event.test.ts new file mode 100644 index 0000000000..e254087bd0 --- /dev/null +++ b/packages/protocol/test/event.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, test } from "bun:test" +import { Event } from "@opencode-ai/schema/event" +import { Schema } from "effect" +import { EventSchema } from "../src/groups/event" + +describe("EventSchema", () => { + test("requires durable metadata on durable events", () => { + expect( + Schema.is(EventSchema)({ + id: Event.ID.create(), + type: "session.created", + data: { sessionID: "session" }, + }), + ).toBe(false) + }) + + test("rejects durable metadata on live events", () => { + expect( + Schema.is(EventSchema)({ + id: Event.ID.create(), + type: "server.connected", + durable: { aggregateID: "aggregate", seq: 0, version: 1 }, + data: {}, + }), + ).toBe(false) + }) +}) diff --git a/packages/schema/src/event.ts b/packages/schema/src/event.ts index 849ffb8902..243667e543 100644 --- a/packages/schema/src/event.ts +++ b/packages/schema/src/event.ts @@ -11,62 +11,119 @@ export const ID = Schema.String.check(Schema.isStartsWith("evt_")).pipe( ) export type ID = typeof ID.Type -export type Definition< +export type DurableOptions = { + readonly version: number + readonly aggregate: string +} + +export type DurableEnvelope = { + readonly aggregateID: string + readonly seq: number + readonly version: number +} + +const PublishedDurableEnvelope = Schema.Struct({ + aggregateID: Schema.String, + seq: Schema.Number, + version: Schema.Number, +}) +const NoDurableEnvelope = Schema.optional(Schema.Never) + +export type LiveDefinition< Type extends string = string, DataSchema extends Schema.Codec = Schema.Codec, > = Schema.Top & { readonly type: Type - readonly durable?: { - readonly version: number - readonly aggregate: string - } readonly data: DataSchema + readonly durable?: never } +export type DurableDefinition< + Type extends string = string, + DataSchema extends Schema.Codec = Schema.Codec, + Durability extends DurableOptions = DurableOptions, +> = Schema.Top & { + readonly type: Type + readonly data: DataSchema + readonly durable: Durability +} + +export type Definition< + Type extends string = string, + DataSchema extends Schema.Codec = Schema.Codec, +> = LiveDefinition | DurableDefinition + export type Data = Schema.Schema.Type -export type Payload = { - readonly id: ID - readonly type: D["type"] - readonly data: Data - readonly durable?: { - readonly aggregateID: string - readonly seq: number - readonly version: number - } - readonly location?: Location.Ref - readonly metadata?: Record -} +export type UncommittedPayload = D extends Definition + ? { + readonly id: ID + readonly type: D["type"] + readonly data: Data + readonly location?: Location.Ref + readonly metadata?: Record + } + : never + +export type PublishedPayload = D extends DurableDefinition + ? UncommittedPayload & { readonly durable: DurableEnvelope } + : D extends LiveDefinition + ? UncommittedPayload & { readonly durable?: never } + : never + +export type Payload = PublishedPayload + +type LiveEventSchema< + Type extends string, + Fields extends Readonly>>, +> = Schema.Schema>>> & + LiveDefinition> + +type DurableEventSchema< + Type extends string, + Fields extends Readonly>>, + Durability extends DurableOptions, +> = Schema.Schema, Durability>>> & + DurableDefinition, Durability> export function define< const Type extends string, Fields extends Readonly>>, +>(input: { readonly type: Type; readonly durable?: never; readonly schema: Fields }): LiveEventSchema +export function define< + const Type extends string, + Fields extends Readonly>>, + const Durability extends DurableOptions, >(input: { readonly type: Type - readonly durable?: { - readonly version: number - readonly aggregate: string - } + readonly durable: Durability readonly schema: Fields -}): Schema.Schema>>> & Definition> { +}): DurableEventSchema +export function define(input: { + readonly type: string + readonly durable?: DurableOptions + readonly schema: Readonly>> +}): Schema.Top { const data = Schema.Struct(input.schema) - return Object.assign( - Schema.Struct({ - id: ID, - metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), - type: Schema.Literal(input.type), - durable: Schema.optional( - Schema.Struct({ aggregateID: Schema.String, seq: Schema.Number, version: Schema.Number }), - ), - location: Schema.optional(Location.Ref), - data, - }).annotate({ identifier: input.type }), - { - type: input.type, - ...(input.durable === undefined ? {} : { durable: input.durable }), - data, - }, - ) as Schema.Schema>>> & Definition> + const fields = { + id: ID, + metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), + type: Schema.Literal(input.type), + location: Schema.optional(Location.Ref), + data, + } + if (input.durable) { + return Object.assign( + Schema.Struct({ ...fields, durable: PublishedDurableEnvelope }).annotate({ + identifier: input.type, + }), + { type: input.type, durable: input.durable, data }, + ) + } + return Object.assign(Schema.Struct({ ...fields, durable: NoDurableEnvelope }).annotate({ identifier: input.type }), { + type: input.type, + data, + }) } export function inventory>(...definitions: Definitions) { @@ -103,7 +160,7 @@ export function durable(definitions: ReadonlyArray) { if (result.has(key)) throw new Error(`Duplicate durable event definition for ${key}`) result.set(key, definition) return result - }, new Map()), + }, new Map()), ) } diff --git a/packages/schema/test/event.test.ts b/packages/schema/test/event.test.ts index 380faa5a4a..02b1feda76 100644 --- a/packages/schema/test/event.test.ts +++ b/packages/schema/test/event.test.ts @@ -34,4 +34,96 @@ describe("public event schemas", () => { expect(Event.durable([definition]).get("test.durable.1")).toBe(definition) }) + + test("durable definitions require published commit metadata", () => { + const definition = Event.define({ + type: "test.durable", + durable: { aggregate: "id", version: 1 }, + schema: { id: Schema.String }, + }) + const payload: typeof definition.Type = { + id: Event.ID.create(), + type: definition.type, + durable: { aggregateID: "aggregate", seq: 0, version: 1 }, + data: { id: "aggregate" }, + } + + expect(Schema.is(definition)(payload)).toBe(true) + expect( + Schema.is(definition)({ + id: Event.ID.create(), + type: definition.type, + data: { id: "aggregate" }, + }), + ).toBe(false) + // @ts-expect-error Published durable payloads require commit metadata. + const missing: typeof definition.Type = { id: Event.ID.create(), type: definition.type, data: { id: "aggregate" } } + void missing + }) + + test("live definitions reject durable commit metadata", () => { + const definition = Event.define({ + type: "test.live", + schema: { value: Schema.String }, + }) + const payload: typeof definition.Type = { + id: Event.ID.create(), + type: definition.type, + data: { value: "value" }, + } + + expect(Schema.is(definition)(payload)).toBe(true) + expect( + Schema.is(definition)({ + ...payload, + durable: { aggregateID: "aggregate", seq: 0, version: 1 }, + }), + ).toBe(false) + + const invalid: typeof definition.Type = { + ...payload, + // @ts-expect-error Live payloads cannot carry durable commit metadata. + durable: { aggregateID: "aggregate", seq: 0, version: 1 }, + } + void invalid + }) + + test("mixed definition payloads preserve durability correlation", () => { + const durable = Event.define({ + type: "test.mixed.durable", + durable: { aggregate: "id", version: 2 }, + schema: { id: Schema.String }, + }) + const live = Event.define({ + type: "test.mixed.live", + schema: { value: Schema.String }, + }) + type Mixed = Event.Payload + + const committed: Mixed = { + id: Event.ID.create(), + type: durable.type, + durable: { aggregateID: "aggregate", seq: 0, version: 2 }, + data: { id: "aggregate" }, + } + const ephemeral: Mixed = { + id: Event.ID.create(), + type: live.type, + data: { value: "value" }, + } + void committed + void ephemeral + + // @ts-expect-error Durable union members require commit metadata. + const uncommitted: Mixed = { id: Event.ID.create(), type: durable.type, data: { id: "aggregate" } } + // @ts-expect-error Live union members cannot carry durable commit metadata. + const falselyCommitted: Mixed = { + id: Event.ID.create(), + type: live.type, + durable: { aggregateID: "aggregate", seq: 0, version: 2 }, + data: { value: "value" }, + } + void uncommitted + void falselyCommitted + }) }) diff --git a/packages/sdk/js/script/build.ts b/packages/sdk/js/script/build.ts index 72f4e3f3e9..2b2032f714 100755 --- a/packages/sdk/js/script/build.ts +++ b/packages/sdk/js/script/build.ts @@ -58,6 +58,17 @@ if (sseTypesPatched === sseTypesSource) { } await Bun.write(sseTypesPath, sseTypesPatched) +// OpenAPI represents Schema.Never as `not: {}`, which @hey-api currently +// widens to unknown. Preserve impossible optional event fields as never. +const eventTypesPath = "./src/v2/gen/types.gen.ts" +const eventTypesFile = Bun.file(eventTypesPath) +const eventTypesSource = await eventTypesFile.text() +const eventTypesPatched = eventTypesSource.replaceAll(" durable?: unknown", " durable?: never") +if (eventTypesPatched === eventTypesSource) { + throw new Error(`Event never patch did not apply; @hey-api/openapi-ts output may have changed (${eventTypesPath})`) +} +await Bun.write(eventTypesPath, eventTypesPatched) + await $`bun prettier --write src/gen` await $`bun prettier --write src/v2` await $`rm -rf dist` diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 2f22dc9e7f..ce480c6ae6 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -4328,12 +4328,8 @@ export type V2EventModelsDevRefreshed = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "models-dev.refreshed" data: { [key: string]: unknown @@ -4345,12 +4341,8 @@ export type V2EventIntegrationUpdated = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "integration.updated" data: { [key: string]: unknown @@ -4362,12 +4354,8 @@ export type V2EventIntegrationConnectionUpdated = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "integration.connection.updated" data: { integrationID: string @@ -4379,12 +4367,8 @@ export type V2EventCatalogUpdated = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "catalog.updated" data: { [key: string]: unknown @@ -4396,12 +4380,12 @@ export type V2EventSessionCreated = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.created" data: { sessionID: string @@ -4414,12 +4398,12 @@ export type V2EventSessionUpdated = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.updated" data: { sessionID: string @@ -4432,12 +4416,12 @@ export type V2EventSessionDeleted = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.deleted" data: { sessionID: string @@ -4450,12 +4434,12 @@ export type V2EventMessageUpdated = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "message.updated" data: { sessionID: string @@ -4468,12 +4452,12 @@ export type V2EventMessageRemoved = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "message.removed" data: { sessionID: string @@ -4486,12 +4470,12 @@ export type V2EventMessagePartUpdated = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "message.part.updated" data: { sessionID: string @@ -4505,12 +4489,12 @@ export type V2EventMessagePartRemoved = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "message.part.removed" data: { sessionID: string @@ -4524,12 +4508,12 @@ export type V2EventSessionNextAgentSwitched = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.next.agent.switched" data: { timestamp: number @@ -4544,12 +4528,12 @@ export type V2EventSessionNextModelSwitched = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.next.model.switched" data: { timestamp: number @@ -4568,12 +4552,12 @@ export type V2EventSessionNextMoved = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.next.moved" data: { timestamp: number @@ -4588,12 +4572,12 @@ export type V2EventSessionNextPrompted = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.next.prompted" data: { timestamp: number @@ -4609,12 +4593,12 @@ export type V2EventSessionNextPromptAdmitted = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.next.prompt.admitted" data: { timestamp: number @@ -4630,12 +4614,12 @@ export type V2EventSessionNextContextUpdated = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.next.context.updated" data: { timestamp: number @@ -4650,12 +4634,12 @@ export type V2EventSessionNextSynthetic = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.next.synthetic" data: { timestamp: number @@ -4670,12 +4654,12 @@ export type V2EventSessionNextShellStarted = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.next.shell.started" data: { timestamp: number @@ -4691,12 +4675,12 @@ export type V2EventSessionNextShellEnded = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.next.shell.ended" data: { timestamp: number @@ -4711,12 +4695,12 @@ export type V2EventSessionNextStepStarted = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.next.step.started" data: { timestamp: number @@ -4737,12 +4721,12 @@ export type V2EventSessionNextStepEnded = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.next.step.ended" data: { timestamp: number @@ -4769,12 +4753,12 @@ export type V2EventSessionNextStepFailed = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.next.step.failed" data: { timestamp: number @@ -4789,12 +4773,12 @@ export type V2EventSessionNextTextStarted = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.next.text.started" data: { timestamp: number @@ -4809,12 +4793,8 @@ export type V2EventSessionNextTextDelta = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "session.next.text.delta" data: { timestamp: number @@ -4830,12 +4810,12 @@ export type V2EventSessionNextTextEnded = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.next.text.ended" data: { timestamp: number @@ -4851,12 +4831,12 @@ export type V2EventSessionNextReasoningStarted = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.next.reasoning.started" data: { timestamp: number @@ -4876,12 +4856,8 @@ export type V2EventSessionNextReasoningDelta = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "session.next.reasoning.delta" data: { timestamp: number @@ -4897,12 +4873,12 @@ export type V2EventSessionNextReasoningEnded = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.next.reasoning.ended" data: { timestamp: number @@ -4923,12 +4899,12 @@ export type V2EventSessionNextToolInputStarted = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.next.tool.input.started" data: { timestamp: number @@ -4944,12 +4920,8 @@ export type V2EventSessionNextToolInputDelta = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "session.next.tool.input.delta" data: { timestamp: number @@ -4965,12 +4937,12 @@ export type V2EventSessionNextToolInputEnded = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.next.tool.input.ended" data: { timestamp: number @@ -4986,12 +4958,12 @@ export type V2EventSessionNextToolCalled = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.next.tool.called" data: { timestamp: number @@ -5018,12 +4990,12 @@ export type V2EventSessionNextToolProgress = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.next.tool.progress" data: { timestamp: number @@ -5042,12 +5014,12 @@ export type V2EventSessionNextToolSuccess = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.next.tool.success" data: { timestamp: number @@ -5076,12 +5048,12 @@ export type V2EventSessionNextToolFailed = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.next.tool.failed" data: { timestamp: number @@ -5106,12 +5078,12 @@ export type V2EventSessionNextRetried = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.next.retried" data: { timestamp: number @@ -5126,12 +5098,12 @@ export type V2EventSessionNextCompactionStarted = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.next.compaction.started" data: { timestamp: number @@ -5146,12 +5118,8 @@ export type V2EventSessionNextCompactionDelta = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "session.next.compaction.delta" data: { timestamp: number @@ -5166,12 +5134,12 @@ export type V2EventSessionNextCompactionEnded = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.next.compaction.ended" data: { timestamp: number @@ -5188,12 +5156,12 @@ export type V2EventSessionNextRevertStaged = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.next.revert.staged" data: { timestamp: number @@ -5213,12 +5181,12 @@ export type V2EventSessionNextRevertCleared = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.next.revert.cleared" data: { timestamp: number @@ -5231,12 +5199,12 @@ export type V2EventSessionNextRevertCommitted = { metadata?: { [key: string]: unknown } - durable?: { + location?: LocationRef + durable: { aggregateID: string seq: number version: number } - location?: LocationRef type: "session.next.revert.committed" data: { timestamp: number @@ -5250,12 +5218,8 @@ export type V2EventMessagePartDelta = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "message.part.delta" data: { sessionID: string @@ -5271,12 +5235,8 @@ export type V2EventSessionDiff = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "session.diff" data: { sessionID: string @@ -5289,12 +5249,8 @@ export type V2EventSessionError = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "session.error" data: { sessionID?: string @@ -5315,12 +5271,8 @@ export type V2EventInstallationUpdated = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "installation.updated" data: { version: string @@ -5332,12 +5284,8 @@ export type V2EventInstallationUpdateAvailable = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "installation.update-available" data: { version: string @@ -5349,12 +5297,8 @@ export type V2EventFileEdited = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "file.edited" data: { file: string @@ -5366,12 +5310,8 @@ export type V2EventReferenceUpdated = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "reference.updated" data: { [key: string]: unknown @@ -5383,12 +5323,8 @@ export type V2EventPermissionV2Asked = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "permission.v2.asked" data: { id: string @@ -5408,12 +5344,8 @@ export type V2EventPermissionV2Replied = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "permission.v2.replied" data: { sessionID: string @@ -5427,12 +5359,8 @@ export type V2EventPluginAdded = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "plugin.added" data: { id: string @@ -5444,12 +5372,8 @@ export type V2EventProjectDirectoriesUpdated = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "project.directories.updated" data: { projectID: string @@ -5461,12 +5385,8 @@ export type V2EventFileWatcherUpdated = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "file.watcher.updated" data: { file: string @@ -5479,12 +5399,8 @@ export type V2EventPtyCreated = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "pty.created" data: { info: Pty @@ -5496,12 +5412,8 @@ export type V2EventPtyUpdated = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "pty.updated" data: { info: Pty @@ -5513,12 +5425,8 @@ export type V2EventPtyExited = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "pty.exited" data: { id: string @@ -5531,12 +5439,8 @@ export type V2EventPtyDeleted = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "pty.deleted" data: { id: string @@ -5548,12 +5452,8 @@ export type V2EventQuestionV2Asked = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "question.v2.asked" data: { id: string @@ -5571,12 +5471,8 @@ export type V2EventQuestionV2Replied = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "question.v2.replied" data: { sessionID: string @@ -5590,12 +5486,8 @@ export type V2EventQuestionV2Rejected = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "question.v2.rejected" data: { sessionID: string @@ -5608,12 +5500,8 @@ export type V2EventTodoUpdated = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "todo.updated" data: { sessionID: string @@ -5626,12 +5514,8 @@ export type V2EventLspUpdated = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "lsp.updated" data: { [key: string]: unknown @@ -5643,12 +5527,8 @@ export type V2EventPermissionAsked = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "permission.asked" data: { id: string @@ -5671,12 +5551,8 @@ export type V2EventPermissionReplied = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "permission.replied" data: { sessionID: string @@ -5690,12 +5566,8 @@ export type V2EventTuiPromptAppend = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "tui.prompt.append" data: { text: string @@ -5707,12 +5579,8 @@ export type V2EventTuiCommandExecute = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "tui.command.execute" data: { command: @@ -5741,12 +5609,8 @@ export type V2EventTuiToastShow = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "tui.toast.show" data: { title?: string @@ -5761,12 +5625,8 @@ export type V2EventTuiSessionSelect = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "tui.session.select" data: { /** @@ -5781,12 +5641,8 @@ export type V2EventMcpToolsChanged = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "mcp.tools.changed" data: { server: string @@ -5798,12 +5654,8 @@ export type V2EventMcpBrowserOpenFailed = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "mcp.browser.open.failed" data: { mcpName: string @@ -5816,12 +5668,8 @@ export type V2EventCommandExecuted = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "command.executed" data: { name: string @@ -5836,12 +5684,8 @@ export type V2EventProjectUpdated = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "project.updated" data: { id: string @@ -5873,12 +5717,8 @@ export type V2EventSessionStatus = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "session.status" data: { sessionID: string @@ -5891,12 +5731,8 @@ export type V2EventSessionIdle = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "session.idle" data: { sessionID: string @@ -5908,12 +5744,8 @@ export type V2EventQuestionAsked = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "question.asked" data: { id: string @@ -5931,12 +5763,8 @@ export type V2EventQuestionReplied = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "question.replied" data: { sessionID: string @@ -5950,12 +5778,8 @@ export type V2EventQuestionRejected = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "question.rejected" data: { sessionID: string @@ -5968,12 +5792,8 @@ export type V2EventSessionCompacted = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "session.compacted" data: { sessionID: string @@ -5985,12 +5805,8 @@ export type V2EventVcsBranchUpdated = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "vcs.branch.updated" data: { branch?: string @@ -6002,12 +5818,8 @@ export type V2EventWorkspaceReady = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "workspace.ready" data: { name: string @@ -6019,12 +5831,8 @@ export type V2EventWorkspaceFailed = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "workspace.failed" data: { message: string @@ -6036,12 +5844,8 @@ export type V2EventWorkspaceStatus = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "workspace.status" data: { workspaceID: string @@ -6054,12 +5858,8 @@ export type V2EventWorktreeReady = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "worktree.ready" data: { name: string @@ -6072,12 +5872,8 @@ export type V2EventWorktreeFailed = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "worktree.failed" data: { message: string @@ -6089,12 +5885,8 @@ export type V2EventServerConnected = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "server.connected" data: { [key: string]: unknown @@ -6106,12 +5898,8 @@ export type V2EventGlobalDisposed = { metadata?: { [key: string]: unknown } - durable?: { - aggregateID: string - seq: number - version: number - } location?: LocationRef + durable?: never type: "global.disposed" data: { [key: string]: unknown