refactor(core): make v2 session inputs event sourced (#30785)

This commit is contained in:
Kit Langton
2026-06-04 19:24:30 -04:00
committed by GitHub
parent 057958c933
commit 76ecf2e58c
43 changed files with 4632 additions and 718 deletions
@@ -1,5 +1,6 @@
import { useEvent } from "@tui/context/event"
import type {
Event,
SessionMessage,
SessionMessageAssistant,
SessionMessageAssistantReasoning,
@@ -54,6 +55,11 @@ function latestReasoning(assistant: SessionMessageAssistant | undefined, reasoni
)
}
function prepend(messages: SessionMessage[], message: SessionMessage) {
if (messages.some((item) => item.id === message.id)) return
messages.unshift(message)
}
export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext({
name: "SyncV2",
init: () => {
@@ -67,6 +73,18 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
const event = useEvent()
const sdk = useSDK()
const applied = new Set<string>()
const buffering = new Map<string, Event[]>()
const syncing = new Map<string, Promise<void>>()
function duplicate(id: string) {
if (applied.has(id)) return true
applied.add(id)
if (applied.size <= 1000) return false
const oldest = applied.values().next()
if (!oldest.done) applied.delete(oldest.value)
return false
}
function update(sessionID: string, fn: (messages: SessionMessage[]) => void) {
setStore(
@@ -77,12 +95,41 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
)
}
event.subscribe((event) => {
async function hydrate(sessionID: string) {
const pending: Event[] = []
const before = JSON.parse(JSON.stringify(store.messages[sessionID] ?? [])) as SessionMessage[]
buffering.set(sessionID, pending)
try {
const response = await sdk.client.v2.session.messages({ sessionID })
const messages = response.data?.data ?? []
const snapshotIDs = new Set(messages.map((message) => message.id))
setStore(
"messages",
sessionID,
reconcile([...messages, ...before.filter((message) => !snapshotIDs.has(message.id))]),
)
buffering.delete(sessionID)
for (const event of pending) apply(event)
} catch (error) {
buffering.delete(sessionID)
throw error
}
}
function sync(sessionID: string) {
const existing = syncing.get(sessionID)
if (existing) return existing
const result = hydrate(sessionID).finally(() => syncing.delete(sessionID))
syncing.set(sessionID, result)
return result
}
function apply(event: Event) {
switch (event.type) {
case "session.next.agent.switched":
update(event.properties.sessionID, (draft) => {
draft.unshift({
id: event.id,
prepend(draft, {
id: event.properties.messageID,
type: "agent-switched",
agent: event.properties.agent,
time: { created: event.properties.timestamp },
@@ -91,8 +138,8 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
break
case "session.next.model.switched":
update(event.properties.sessionID, (draft) => {
draft.unshift({
id: event.id,
prepend(draft, {
id: event.properties.messageID,
type: "model-switched",
model: event.properties.model,
time: { created: event.properties.timestamp },
@@ -101,8 +148,8 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
break
case "session.next.prompted": {
update(event.properties.sessionID, (draft) => {
draft.unshift({
id: event.id,
prepend(draft, {
id: event.properties.messageID,
type: "user",
text: event.properties.prompt.text,
files: event.properties.prompt.files,
@@ -113,10 +160,25 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
})
break
}
case "session.next.prompt.admitted":
break
case "session.next.prompt.promoted":
update(event.properties.sessionID, (draft) => {
prepend(draft, {
id: event.properties.messageID,
type: "user",
text: event.properties.prompt.text,
files: event.properties.prompt.files,
agents: event.properties.prompt.agents,
references: event.properties.prompt.references,
time: { created: event.properties.timeCreated },
})
})
break
case "session.next.synthetic":
update(event.properties.sessionID, (draft) => {
draft.unshift({
id: event.id,
prepend(draft, {
id: event.properties.messageID,
type: "synthetic",
sessionID: event.properties.sessionID,
text: event.properties.text,
@@ -126,8 +188,8 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
break
case "session.next.shell.started":
update(event.properties.sessionID, (draft) => {
draft.unshift({
id: event.id,
prepend(draft, {
id: event.properties.messageID,
type: "shell",
callID: event.properties.callID,
command: event.properties.command,
@@ -146,10 +208,11 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
break
case "session.next.step.started":
update(event.properties.sessionID, (draft) => {
if (draft.some((message) => message.id === event.properties.assistantMessageID)) return
const currentAssistant = activeAssistant(draft)
if (currentAssistant) currentAssistant.time.completed = event.properties.timestamp
draft.unshift({
id: event.id,
prepend(draft, {
id: event.properties.assistantMessageID,
type: "assistant",
agent: event.properties.agent,
model: event.properties.model,
@@ -182,18 +245,28 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
break
case "session.next.text.started":
update(event.properties.sessionID, (draft) => {
activeAssistant(draft)?.content.push({ type: "text", id: event.properties.textID, text: "" })
ownedAssistant(draft, event.properties.assistantMessageID)?.content.push({
type: "text",
id: event.properties.textID,
text: "",
})
})
break
case "session.next.text.delta":
update(event.properties.sessionID, (draft) => {
const match = latestText(activeAssistant(draft), event.properties.textID)
const match = latestText(
ownedAssistant(draft, event.properties.assistantMessageID),
event.properties.textID,
)
if (match) match.text += event.properties.delta
})
break
case "session.next.text.ended":
update(event.properties.sessionID, (draft) => {
const match = latestText(activeAssistant(draft), event.properties.textID)
const match = latestText(
ownedAssistant(draft, event.properties.assistantMessageID),
event.properties.textID,
)
if (match) match.text = event.properties.text
})
break
@@ -263,7 +336,11 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
content: [...event.properties.content],
result: event.properties.result,
}
match.provider = event.properties.provider
match.provider = {
executed: event.properties.provider.executed || match.provider?.executed === true,
metadata: match.provider?.metadata,
resultMetadata: event.properties.provider.metadata,
}
match.time.completed = event.properties.timestamp
})
break
@@ -282,13 +359,17 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
content: match.state.status === "running" ? match.state.content : [],
result: event.properties.result,
}
match.provider = event.properties.provider
match.provider = {
executed: event.properties.provider.executed || match.provider?.executed === true,
metadata: match.provider?.metadata,
resultMetadata: event.properties.provider.metadata,
}
match.time.completed = event.properties.timestamp
})
break
case "session.next.reasoning.started":
update(event.properties.sessionID, (draft) => {
activeAssistant(draft)?.content.push({
ownedAssistant(draft, event.properties.assistantMessageID)?.content.push({
type: "reasoning",
id: event.properties.reasoningID,
text: "",
@@ -298,13 +379,19 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
break
case "session.next.reasoning.delta":
update(event.properties.sessionID, (draft) => {
const match = latestReasoning(activeAssistant(draft), event.properties.reasoningID)
const match = latestReasoning(
ownedAssistant(draft, event.properties.assistantMessageID),
event.properties.reasoningID,
)
if (match) match.text += event.properties.delta
})
break
case "session.next.reasoning.ended":
update(event.properties.sessionID, (draft) => {
const match = latestReasoning(activeAssistant(draft), event.properties.reasoningID)
const match = latestReasoning(
ownedAssistant(draft, event.properties.assistantMessageID),
event.properties.reasoningID,
)
if (match) {
match.text = event.properties.text
if (event.properties.providerMetadata !== undefined)
@@ -316,8 +403,8 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
break
case "session.next.compaction.started":
update(event.properties.sessionID, (draft) => {
draft.unshift({
id: event.id,
prepend(draft, {
id: event.properties.messageID,
type: "compaction",
reason: event.properties.reason,
summary: "",
@@ -340,16 +427,20 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
})
break
}
}
event.subscribe((event) => {
if (duplicate(event.id)) return
if ("sessionID" in event.properties && typeof event.properties.sessionID === "string")
buffering.get(event.properties.sessionID)?.push(event)
apply(event)
})
const result = {
data: store,
session: {
message: {
async sync(sessionID: string) {
const response = await sdk.client.v2.session.messages({ sessionID })
setStore("messages", sessionID, reconcile(response.data?.data ?? []))
},
sync,
fromSession(sessionID: string) {
const messages = store.messages[sessionID]
if (!messages) return []
@@ -32,7 +32,7 @@ const EventSchema = Schema.Union([
.values()
.map((definition) =>
Schema.Struct({
id: Schema.String,
id: EventV2.ID,
type: Schema.Literal(definition.type),
properties: definition.data,
}).annotate({ identifier: `Event.${definition.type}` }),
@@ -20,10 +20,10 @@ const SyncEventSchemas = EventV2.registry
return [
Schema.Struct({
type: Schema.Literal("sync"),
id: Schema.String,
id: EventV2.ID,
syncEvent: Schema.Struct({
type: Schema.Literal(EventV2.versionedType(definition.type, definition.sync.version)),
id: Schema.String,
id: EventV2.ID,
seq: Schema.Finite,
aggregateID: Schema.String,
data: definition.data,
@@ -41,7 +41,7 @@ const GlobalEventSchema = Schema.Struct({
...EventV2.registry
.values()
.map((definition) =>
Schema.Struct({ id: Schema.String, type: Schema.Literal(definition.type), properties: definition.data }),
Schema.Struct({ id: EventV2.ID, type: Schema.Literal(definition.type), properties: definition.data }),
)
.toArray(),
InstanceDisposed,
@@ -1,4 +1,5 @@
import { NonNegativeInt } from "@opencode-ai/core/schema"
import { EventV2 } from "@opencode-ai/core/event"
import { SessionID } from "@/session/schema"
import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
@@ -9,7 +10,7 @@ import { described } from "./metadata"
const root = "/sync"
export const ReplayEvent = Schema.Struct({
id: Schema.String,
id: EventV2.ID,
aggregateID: Schema.String,
seq: NonNegativeInt,
type: Schema.String,
@@ -27,7 +28,7 @@ export const SessionPayload = Schema.Struct({
})
export const HistoryPayload = Schema.Record(Schema.String, NonNegativeInt)
export const HistoryEvent = Schema.Struct({
id: Schema.String,
id: EventV2.ID,
aggregate_id: Schema.String,
seq: NonNegativeInt,
type: Schema.String,
@@ -36,7 +36,7 @@ export const syncHandlers = HttpApiBuilder.group(InstanceHttpApi, "sync", (handl
const replay = Effect.fn("SyncHttpApi.replay")(function* (ctx: { payload: typeof ReplayPayload.Type }) {
const payload: EventV2.SerializedEvent[] = ctx.payload.events.map((event) => ({
id: EventV2.ID.make(event.id),
id: event.id,
aggregateID: event.aggregateID,
seq: event.seq,
type: event.type,
+3 -1
View File
@@ -12,7 +12,7 @@ import { Plugin } from "@/plugin"
import { Config } from "@/config/config"
import { NotFoundError } from "@/storage/storage"
import { Effect, Layer, Context, Schema } from "effect"
import { Effect, Layer, Context } from "effect"
import * as DateTime from "effect/DateTime"
import { InstanceState } from "@/effect/instance-state"
import { isOverflow as overflow, usable } from "./overflow"
@@ -20,6 +20,7 @@ import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { EventV2Bridge } from "@/event-v2-bridge"
import { SessionEvent } from "@opencode-ai/core/session/event"
import { SessionMessage } from "@opencode-ai/core/session/message"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { EventV2 } from "@opencode-ai/core/event"
@@ -609,6 +610,7 @@ export const layer = Layer.effect(
if (flags.experimentalEventSystem) {
yield* events.publish(SessionEvent.Compaction.Started, {
sessionID: input.sessionID,
messageID: SessionMessage.ID.create(),
timestamp: DateTime.makeUnsafe(Date.now()),
reason: input.auto ? "auto" : "manual",
})
+47 -34
View File
@@ -25,13 +25,13 @@ import { isRecord } from "@/util/record"
import { EventV2Bridge } from "@/event-v2-bridge"
import { Database } from "@opencode-ai/core/database/database"
import { SessionEvent } from "@opencode-ai/core/session/event"
import { SessionMessage } from "@opencode-ai/core/session/message"
import { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider"
import * as DateTime from "effect/DateTime"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { toolFileSourceFromUri, Usage, type LLMEvent } from "@opencode-ai/llm"
import { ToolOutput } from "@opencode-ai/core/tool-output"
import type { EventV2 } from "@opencode-ai/core/event"
const DOOM_LOOP_THRESHOLD = 3
const log = Log.create({ service: "session.processor" })
@@ -67,7 +67,7 @@ export interface Interface {
}
type ToolCall = {
assistantMessageID?: EventV2.ID
assistantMessageID?: SessionMessage.ID
partID: SessionV1.ToolPart["id"]
messageID: SessionV1.ToolPart["messageID"]
sessionID: SessionV1.ToolPart["sessionID"]
@@ -85,7 +85,7 @@ interface ProcessorContext extends Input {
currentText: SessionV1.TextPart | undefined
currentTextID: string | undefined
reasoningMap: Record<string, SessionV1.ReasoningPart>
v2AssistantMessageID: EventV2.ID | undefined
v2AssistantMessageID: SessionMessage.ID | undefined
}
type StreamEvent = LLMEvent
@@ -129,6 +129,7 @@ export const layer = Layer.effect(
reasoningMap: {},
v2AssistantMessageID: undefined,
}
const mirrorAssistant = flags.experimentalEventSystem && !input.assistantMessage.summary
let aborted = false
const slog = log.clone().tag("session.id", input.sessionID).tag("messageID", input.assistantMessage.id)
@@ -146,8 +147,10 @@ export const layer = Layer.effect(
const ensureV2AssistantMessage = Effect.fn("SessionProcessor.ensureV2AssistantMessage")(function* () {
if (ctx.v2AssistantMessageID) return ctx.v2AssistantMessageID
ctx.v2AssistantMessageID = (yield* events.publish(SessionEvent.Step.Started, {
ctx.v2AssistantMessageID = SessionMessage.ID.create()
yield* events.publish(SessionEvent.Step.Started, {
sessionID: ctx.sessionID,
assistantMessageID: ctx.v2AssistantMessageID,
agent: input.assistantMessage.agent,
model: {
id: ModelV2.ID.make(ctx.model.id),
@@ -156,7 +159,7 @@ export const layer = Layer.effect(
},
snapshot: ctx.snapshot,
timestamp: DateTime.makeUnsafe(Date.now()),
})).id
})
return ctx.v2AssistantMessageID
})
@@ -249,9 +252,10 @@ export const layer = Layer.effect(
const finishReasoning = Effect.fn("SessionProcessor.finishReasoning")(function* (reasoningID: string) {
if (!(reasoningID in ctx.reasoningMap)) return
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (flags.experimentalEventSystem) {
if (mirrorAssistant) {
yield* events.publish(SessionEvent.Reasoning.Ended, {
sessionID: ctx.sessionID,
assistantMessageID: yield* currentV2AssistantMessage(),
reasoningID,
text: ctx.reasoningMap[reasoningID].text,
providerMetadata: ctx.reasoningMap[reasoningID].metadata,
@@ -266,23 +270,29 @@ export const layer = Layer.effect(
})
const flushV2Fragments = Effect.fn("SessionProcessor.flushV2Fragments")(function* () {
if (!flags.experimentalEventSystem) return
if (!mirrorAssistant) return
if (!ctx.assistantMessage.summary && ctx.currentText && ctx.currentTextID) {
yield* events.publish(SessionEvent.Text.Ended, {
sessionID: ctx.sessionID,
assistantMessageID: yield* currentV2AssistantMessage(),
textID: ctx.currentTextID,
text: ctx.currentText.text,
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
yield* Effect.forEach(Object.entries(ctx.reasoningMap), ([reasoningID, part]) =>
events.publish(SessionEvent.Reasoning.Ended, {
sessionID: ctx.sessionID,
reasoningID,
text: part.text,
providerMetadata: part.metadata,
timestamp: DateTime.makeUnsafe(Date.now()),
}),
currentV2AssistantMessage().pipe(
Effect.flatMap((assistantMessageID) =>
events.publish(SessionEvent.Reasoning.Ended, {
sessionID: ctx.sessionID,
assistantMessageID,
reasoningID,
text: part.text,
providerMetadata: part.metadata,
timestamp: DateTime.makeUnsafe(Date.now()),
}),
),
),
)
})
@@ -307,7 +317,7 @@ export const layer = Layer.effect(
return { call: ctx.toolcalls[input.id], part }
}
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
const assistantMessageID = flags.experimentalEventSystem ? yield* ensureV2AssistantMessage() : undefined
const assistantMessageID = mirrorAssistant ? yield* ensureV2AssistantMessage() : undefined
if (assistantMessageID) {
yield* events.publish(SessionEvent.Tool.Input.Started, {
sessionID: ctx.sessionID,
@@ -367,9 +377,10 @@ export const layer = Layer.effect(
case "reasoning-start":
if (value.id in ctx.reasoningMap) return
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (flags.experimentalEventSystem) {
if (mirrorAssistant) {
yield* events.publish(SessionEvent.Reasoning.Started, {
sessionID: ctx.sessionID,
assistantMessageID: yield* ensureV2AssistantMessage(),
reasoningID: value.id,
providerMetadata: value.providerMetadata,
timestamp: DateTime.makeUnsafe(Date.now()),
@@ -392,9 +403,10 @@ export const layer = Layer.effect(
if (!(value.id in ctx.reasoningMap)) return
ctx.reasoningMap[value.id].text += value.text
if (value.providerMetadata) ctx.reasoningMap[value.id].metadata = value.providerMetadata
if (flags.experimentalEventSystem) {
if (mirrorAssistant) {
yield* events.publish(SessionEvent.Reasoning.Delta, {
sessionID: ctx.sessionID,
assistantMessageID: yield* currentV2AssistantMessage(),
reasoningID: value.id,
delta: value.text,
timestamp: DateTime.makeUnsafe(Date.now()),
@@ -426,9 +438,7 @@ export const layer = Layer.effect(
case "tool-input-delta":
{
const toolCall = yield* ensureToolCall(value)
const assistantMessageID = flags.experimentalEventSystem
? yield* requireV2AssistantMessage(toolCall.call)
: undefined
const assistantMessageID = mirrorAssistant ? yield* requireV2AssistantMessage(toolCall.call) : undefined
if (assistantMessageID) {
yield* events.publish(SessionEvent.Tool.Input.Delta, {
sessionID: ctx.sessionID,
@@ -445,7 +455,7 @@ export const layer = Layer.effect(
case "tool-input-end": {
const toolCall = yield* ensureToolCall(value)
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (flags.experimentalEventSystem) {
if (mirrorAssistant) {
const assistantMessageID = yield* requireV2AssistantMessage(toolCall.call)
yield* events.publish(SessionEvent.Tool.Input.Ended, {
sessionID: ctx.sessionID,
@@ -467,7 +477,7 @@ export const layer = Layer.effect(
const input = isRecord(value.input) ? value.input : { value: value.input }
if (!toolCall.call.inputEnded) {
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (flags.experimentalEventSystem) {
if (mirrorAssistant) {
const assistantMessageID = yield* requireV2AssistantMessage(toolCall.call)
yield* events.publish(SessionEvent.Tool.Input.Ended, {
sessionID: ctx.sessionID,
@@ -479,7 +489,7 @@ export const layer = Layer.effect(
}
}
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (flags.experimentalEventSystem) {
if (mirrorAssistant) {
const assistantMessageID = yield* requireV2AssistantMessage(toolCall.call)
yield* events.publish(SessionEvent.Tool.Called, {
sessionID: ctx.sessionID,
@@ -545,7 +555,7 @@ export const layer = Layer.effect(
if (!toolCall && value.result.type === "error") return
if (value.result.type === "error") {
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (flags.experimentalEventSystem) {
if (mirrorAssistant) {
const assistantMessageID = yield* requireV2AssistantMessage(toolCall?.call)
yield* events.publish(SessionEvent.Tool.Failed, {
sessionID: ctx.sessionID,
@@ -586,7 +596,7 @@ export const layer = Layer.effect(
attachments: attachments.length ? attachments : undefined,
}
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (flags.experimentalEventSystem) {
if (mirrorAssistant) {
const assistantMessageID = yield* requireV2AssistantMessage(toolCall?.call)
const content = [
ToolOutput.text({ type: "text", text: output.output }),
@@ -642,7 +652,7 @@ export const layer = Layer.effect(
case "tool-error": {
const toolCall = yield* readToolCall(value.id)
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (flags.experimentalEventSystem) {
if (mirrorAssistant) {
const assistantMessageID = yield* requireV2AssistantMessage(toolCall?.call)
yield* events.publish(SessionEvent.Tool.Failed, {
sessionID: ctx.sessionID,
@@ -670,7 +680,7 @@ export const layer = Layer.effect(
if (!ctx.snapshot) ctx.snapshot = yield* snapshot.track()
if (!ctx.assistantMessage.summary) {
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (flags.experimentalEventSystem) {
if (mirrorAssistant) {
yield* ensureV2AssistantMessage()
}
}
@@ -693,7 +703,7 @@ export const layer = Layer.effect(
})
if (!ctx.assistantMessage.summary) {
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (flags.experimentalEventSystem) {
if (mirrorAssistant) {
yield* events.publish(SessionEvent.Step.Ended, {
sessionID: ctx.sessionID,
assistantMessageID: yield* currentV2AssistantMessage(),
@@ -752,9 +762,10 @@ export const layer = Layer.effect(
case "text-start":
if (!ctx.assistantMessage.summary) {
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (flags.experimentalEventSystem) {
if (mirrorAssistant) {
yield* events.publish(SessionEvent.Text.Started, {
sessionID: ctx.sessionID,
assistantMessageID: yield* ensureV2AssistantMessage(),
timestamp: DateTime.makeUnsafe(Date.now()),
textID: value.id,
})
@@ -777,9 +788,10 @@ export const layer = Layer.effect(
if (!ctx.currentText) return
ctx.currentText.text += value.text
if (value.providerMetadata) ctx.currentText.metadata = value.providerMetadata
if (flags.experimentalEventSystem) {
if (mirrorAssistant) {
yield* events.publish(SessionEvent.Text.Delta, {
sessionID: ctx.sessionID,
assistantMessageID: yield* currentV2AssistantMessage(),
textID: value.id,
delta: value.text,
timestamp: DateTime.makeUnsafe(Date.now()),
@@ -809,9 +821,10 @@ export const layer = Layer.effect(
)).text
if (!ctx.assistantMessage.summary) {
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (flags.experimentalEventSystem) {
if (mirrorAssistant) {
yield* events.publish(SessionEvent.Text.Ended, {
sessionID: ctx.sessionID,
assistantMessageID: yield* currentV2AssistantMessage(),
text: ctx.currentText.text,
timestamp: DateTime.makeUnsafe(Date.now()),
textID: value.id,
@@ -876,7 +889,7 @@ export const layer = Layer.effect(
const match = yield* readToolCall(toolCallID)
if (!match) continue
const part = match.part
if (flags.experimentalEventSystem && match.call.assistantMessageID) {
if (mirrorAssistant && match.call.assistantMessageID) {
yield* events.publish(SessionEvent.Tool.Failed, {
sessionID: ctx.sessionID,
assistantMessageID: match.call.assistantMessageID,
@@ -922,7 +935,7 @@ export const layer = Layer.effect(
}
if (!ctx.assistantMessage.summary) {
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (flags.experimentalEventSystem) {
if (mirrorAssistant) {
yield* events.publish(SessionEvent.Step.Failed, {
sessionID: ctx.sessionID,
assistantMessageID: yield* ensureV2AssistantMessage(),
@@ -979,7 +992,7 @@ export const layer = Layer.effect(
parse,
set: (info) => {
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
const event = flags.experimentalEventSystem
const event = mirrorAssistant
? events.publish(SessionEvent.Retried, {
sessionID: ctx.sessionID,
attempt: info.attempt,
+6
View File
@@ -51,6 +51,7 @@ import { RuntimeFlags } from "@/effect/runtime-flags"
import { EventV2Bridge } from "@/event-v2-bridge"
import { Database } from "@opencode-ai/core/database/database"
import { SessionEvent } from "@opencode-ai/core/session/event"
import { SessionMessage } from "@opencode-ai/core/session/message"
import { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { AgentAttachment, FileAttachment, Prompt, ReferenceAttachment, Source } from "@opencode-ai/core/session/prompt"
@@ -563,6 +564,7 @@ export const layer = Layer.effect(
if (flags.experimentalEventSystem) {
yield* events.publish(SessionEvent.Shell.Started, {
sessionID: input.sessionID,
messageID: SessionMessage.ID.create(),
timestamp: DateTime.makeUnsafe(started),
callID: part.callID,
command: input.command,
@@ -738,6 +740,7 @@ export const layer = Layer.effect(
if (current?.agent !== info.agent) {
yield* events.publish(SessionEvent.AgentSwitched, {
sessionID: input.sessionID,
messageID: SessionMessage.ID.create(),
timestamp: DateTime.makeUnsafe(info.time.created),
agent: info.agent,
})
@@ -749,6 +752,7 @@ export const layer = Layer.effect(
) {
yield* events.publish(SessionEvent.ModelSwitched, {
sessionID: input.sessionID,
messageID: SessionMessage.ID.create(),
timestamp: DateTime.makeUnsafe(info.time.created),
model: {
id: ModelV2.ID.make(info.model.modelID),
@@ -1190,6 +1194,7 @@ export const layer = Layer.effect(
if (flags.experimentalEventSystem) {
yield* events.publish(SessionEvent.Prompted, {
sessionID: input.sessionID,
messageID: SessionMessage.ID.create(),
timestamp: DateTime.makeUnsafe(info.time.created),
delivery: "steer",
prompt: new Prompt({
@@ -1205,6 +1210,7 @@ export const layer = Layer.effect(
if (flags.experimentalEventSystem) {
yield* events.publish(SessionEvent.Synthetic, {
sessionID: input.sessionID,
messageID: SessionMessage.ID.create(),
timestamp: DateTime.makeUnsafe(info.time.created),
text,
})