refactor(schema): session shell payloads and event prefix restore (#35229)

This commit is contained in:
Kit Langton
2026-07-03 17:30:25 -04:00
committed by GitHub
parent 64e4f6f91b
commit 650d774372
49 changed files with 1521 additions and 1200 deletions
@@ -177,7 +177,7 @@ export function DialogCustomProvider(props: Props) {
> >
<div class="flex flex-col gap-6 px-2.5 pb-3 overflow-y-auto max-h-[60vh]"> <div class="flex flex-col gap-6 px-2.5 pb-3 overflow-y-auto max-h-[60vh]">
<div class="px-2.5 flex gap-4 items-center"> <div class="px-2.5 flex gap-4 items-center">
<ProviderIcon id="synthetic" class="size-5 shrink-0 icon-strong-base" /> <ProviderIcon id="session.synthetic" class="size-5 shrink-0 icon-strong-base" />
<div class="text-16-medium text-text-strong">{language.t("provider.custom.title")}</div> <div class="text-16-medium text-text-strong">{language.t("provider.custom.title")}</div>
</div> </div>
@@ -226,7 +226,7 @@ const SettingsProvidersContent: Component = () => {
> >
<div class="flex flex-col min-w-0"> <div class="flex flex-col min-w-0">
<div class="flex flex-wrap items-center gap-x-3 gap-y-1"> <div class="flex flex-wrap items-center gap-x-3 gap-y-1">
<ProviderIcon id="synthetic" class="size-5 shrink-0 icon-strong-base" /> <ProviderIcon id="session.synthetic" class="size-5 shrink-0 icon-strong-base" />
<span class="text-14-medium text-text-strong">{language.t("provider.custom.title")}</span> <span class="text-14-medium text-text-strong">{language.t("provider.custom.title")}</span>
<Tag>{language.t("settings.providers.tag.custom")}</Tag> <Tag>{language.t("settings.providers.tag.custom")}</Tag>
</div> </div>
@@ -223,7 +223,7 @@ export const SettingsProvidersV2: Component = () => {
<div class="settings-v2-provider-row" data-component="custom-provider-section"> <div class="settings-v2-provider-row" data-component="custom-provider-section">
<div class="settings-v2-provider-lead"> <div class="settings-v2-provider-lead">
<ProviderIcon <ProviderIcon
id="synthetic" id="session.synthetic"
width={PROVIDER_ICON_SIZE} width={PROVIDER_ICON_SIZE}
height={PROVIDER_ICON_SIZE} height={PROVIDER_ICON_SIZE}
class="settings-v2-provider-icon shrink-0" class="settings-v2-provider-icon shrink-0"
+198 -76
View File
@@ -976,9 +976,27 @@ export type SessionContextOutput = {
readonly metadata?: { readonly [x: string]: JsonValue } readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number; readonly completed?: number } readonly time: { readonly created: number; readonly completed?: number }
readonly type: "shell" readonly type: "shell"
readonly callID: string readonly shell: {
readonly command: string readonly id: string
readonly output: string readonly status: "running" | "exited" | "timeout" | "killed"
readonly command: string
readonly cwd: string
readonly shell: string
readonly file: string
readonly pid?: number
readonly exit?: number | "Infinity" | "-Infinity" | "NaN"
readonly metadata: { readonly [x: string]: JsonValue }
readonly time: {
readonly started: number | "Infinity" | "-Infinity" | "NaN"
readonly completed?: number | "Infinity" | "-Infinity" | "NaN"
}
}
readonly output?: {
readonly output: string
readonly cursor: number
readonly size: number
readonly truncated: boolean
}
} }
| { | {
readonly id: string readonly id: string
@@ -1109,7 +1127,7 @@ export type SessionLogOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "agent.selected" readonly type: "session.agent.selected"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly agent: string } readonly data: { readonly sessionID: string; readonly agent: string }
@@ -1118,7 +1136,7 @@ export type SessionLogOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "model.selected" readonly type: "session.model.selected"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -1143,7 +1161,7 @@ export type SessionLogOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "renamed" readonly type: "session.renamed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly title: string } readonly data: { readonly sessionID: string; readonly title: string }
@@ -1152,7 +1170,7 @@ export type SessionLogOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "forked" readonly type: "session.forked"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly parentID: string; readonly from?: string } readonly data: { readonly sessionID: string; readonly parentID: string; readonly from?: string }
@@ -1161,7 +1179,7 @@ export type SessionLogOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "prompt.promoted" readonly type: "session.prompt.promoted"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly inputID: string } readonly data: { readonly sessionID: string; readonly inputID: string }
@@ -1170,7 +1188,7 @@ export type SessionLogOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "prompt.admitted" readonly type: "session.prompt.admitted"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -1206,7 +1224,7 @@ export type SessionLogOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "synthetic" readonly type: "session.synthetic"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -1220,7 +1238,7 @@ export type SessionLogOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "skill.activated" readonly type: "session.skill.activated"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly name: string; readonly text: string } readonly data: { readonly sessionID: string; readonly name: string; readonly text: string }
@@ -1229,25 +1247,59 @@ export type SessionLogOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "shell.started" readonly type: "session.shell.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly callID: string; readonly command: string } readonly data: {
readonly sessionID: string
readonly shell: {
readonly id: string
readonly status: "running" | "exited" | "timeout" | "killed"
readonly command: string
readonly cwd: string
readonly shell: string
readonly file: string
readonly pid?: number
readonly exit?: number
readonly metadata: { readonly [x: string]: unknown }
readonly time: { readonly started: number; readonly completed?: number }
}
}
} }
| { | {
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "shell.ended" readonly type: "session.shell.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly callID: string; readonly output: string } readonly data: {
readonly sessionID: string
readonly shell: {
readonly id: string
readonly status: "running" | "exited" | "timeout" | "killed"
readonly command: string
readonly cwd: string
readonly shell: string
readonly file: string
readonly pid?: number
readonly exit?: number
readonly metadata: { readonly [x: string]: unknown }
readonly time: { readonly started: number; readonly completed?: number }
}
readonly output: {
readonly output: string
readonly cursor: number
readonly size: number
readonly truncated: boolean
}
}
} }
| { | {
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "step.started" readonly type: "session.step.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -1262,7 +1314,7 @@ export type SessionLogOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "step.ended" readonly type: "session.step.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -1284,7 +1336,7 @@ export type SessionLogOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "step.failed" readonly type: "session.step.failed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -1297,7 +1349,7 @@ export type SessionLogOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "text.started" readonly type: "session.text.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly assistantMessageID: string; readonly textID: string } readonly data: { readonly sessionID: string; readonly assistantMessageID: string; readonly textID: string }
@@ -1306,7 +1358,7 @@ export type SessionLogOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "text.ended" readonly type: "session.text.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -1320,7 +1372,7 @@ export type SessionLogOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "reasoning.started" readonly type: "session.reasoning.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -1334,7 +1386,7 @@ export type SessionLogOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "reasoning.ended" readonly type: "session.reasoning.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -1349,7 +1401,7 @@ export type SessionLogOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "tool.input.started" readonly type: "session.tool.input.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -1363,7 +1415,7 @@ export type SessionLogOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "tool.input.ended" readonly type: "session.tool.input.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -1377,7 +1429,7 @@ export type SessionLogOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "tool.called" readonly type: "session.tool.called"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -1396,7 +1448,7 @@ export type SessionLogOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "tool.progress" readonly type: "session.tool.progress"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -1414,7 +1466,7 @@ export type SessionLogOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "tool.success" readonly type: "session.tool.success"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -1438,7 +1490,7 @@ export type SessionLogOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "tool.failed" readonly type: "session.tool.failed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -1457,7 +1509,7 @@ export type SessionLogOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "retried" readonly type: "session.retried"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -1477,7 +1529,7 @@ export type SessionLogOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "compaction.started" readonly type: "session.compaction.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly reason: "auto" | "manual" } readonly data: { readonly sessionID: string; readonly reason: "auto" | "manual" }
@@ -1486,7 +1538,7 @@ export type SessionLogOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "compaction.ended" readonly type: "session.compaction.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -1500,7 +1552,7 @@ export type SessionLogOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "revert.staged" readonly type: "session.revert.staged"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -1524,7 +1576,7 @@ export type SessionLogOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "revert.cleared" readonly type: "session.revert.cleared"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string } readonly data: { readonly sessionID: string }
@@ -1533,7 +1585,7 @@ export type SessionLogOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "revert.committed" readonly type: "session.revert.committed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly messageID: string } readonly data: { readonly sessionID: string; readonly messageID: string }
@@ -1617,9 +1669,27 @@ export type SessionMessageOutput = {
readonly metadata?: { readonly [x: string]: JsonValue } readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number; readonly completed?: number } readonly time: { readonly created: number; readonly completed?: number }
readonly type: "shell" readonly type: "shell"
readonly callID: string readonly shell: {
readonly command: string readonly id: string
readonly output: string readonly status: "running" | "exited" | "timeout" | "killed"
readonly command: string
readonly cwd: string
readonly shell: string
readonly file: string
readonly pid?: number
readonly exit?: number | "Infinity" | "-Infinity" | "NaN"
readonly metadata: { readonly [x: string]: JsonValue }
readonly time: {
readonly started: number | "Infinity" | "-Infinity" | "NaN"
readonly completed?: number | "Infinity" | "-Infinity" | "NaN"
}
}
readonly output?: {
readonly output: string
readonly cursor: number
readonly size: number
readonly truncated: boolean
}
} }
| { | {
readonly id: string readonly id: string
@@ -1798,9 +1868,27 @@ export type MessageListOutput = {
readonly metadata?: { readonly [x: string]: JsonValue } readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number; readonly completed?: number } readonly time: { readonly created: number; readonly completed?: number }
readonly type: "shell" readonly type: "shell"
readonly callID: string readonly shell: {
readonly command: string readonly id: string
readonly output: string readonly status: "running" | "exited" | "timeout" | "killed"
readonly command: string
readonly cwd: string
readonly shell: string
readonly file: string
readonly pid?: number
readonly exit?: number | "Infinity" | "-Infinity" | "NaN"
readonly metadata: { readonly [x: string]: JsonValue }
readonly time: {
readonly started: number | "Infinity" | "-Infinity" | "NaN"
readonly completed?: number | "Infinity" | "-Infinity" | "NaN"
}
}
readonly output?: {
readonly output: string
readonly cursor: number
readonly size: number
readonly truncated: boolean
}
} }
| { | {
readonly id: string readonly id: string
@@ -4309,7 +4397,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "agent.selected" readonly type: "session.agent.selected"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly agent: string } readonly data: { readonly sessionID: string; readonly agent: string }
@@ -4318,7 +4406,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "model.selected" readonly type: "session.model.selected"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -4343,7 +4431,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "renamed" readonly type: "session.renamed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly title: string } readonly data: { readonly sessionID: string; readonly title: string }
@@ -4352,7 +4440,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "forked" readonly type: "session.forked"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly parentID: string; readonly from?: string } readonly data: { readonly sessionID: string; readonly parentID: string; readonly from?: string }
@@ -4361,7 +4449,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "prompt.promoted" readonly type: "session.prompt.promoted"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly inputID: string } readonly data: { readonly sessionID: string; readonly inputID: string }
@@ -4370,7 +4458,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "prompt.admitted" readonly type: "session.prompt.admitted"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -4397,7 +4485,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "execution.settled" readonly type: "session.execution.settled"
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
readonly sessionID: string readonly sessionID: string
@@ -4418,7 +4506,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "synthetic" readonly type: "session.synthetic"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -4432,7 +4520,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "skill.activated" readonly type: "session.skill.activated"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly name: string; readonly text: string } readonly data: { readonly sessionID: string; readonly name: string; readonly text: string }
@@ -4441,25 +4529,59 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "shell.started" readonly type: "session.shell.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly callID: string; readonly command: string } readonly data: {
readonly sessionID: string
readonly shell: {
readonly id: string
readonly status: "running" | "exited" | "timeout" | "killed"
readonly command: string
readonly cwd: string
readonly shell: string
readonly file: string
readonly pid?: number
readonly exit?: number
readonly metadata: { readonly [x: string]: unknown }
readonly time: { readonly started: number; readonly completed?: number }
}
}
} }
| { | {
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "shell.ended" readonly type: "session.shell.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly callID: string; readonly output: string } readonly data: {
readonly sessionID: string
readonly shell: {
readonly id: string
readonly status: "running" | "exited" | "timeout" | "killed"
readonly command: string
readonly cwd: string
readonly shell: string
readonly file: string
readonly pid?: number
readonly exit?: number
readonly metadata: { readonly [x: string]: unknown }
readonly time: { readonly started: number; readonly completed?: number }
}
readonly output: {
readonly output: string
readonly cursor: number
readonly size: number
readonly truncated: boolean
}
}
} }
| { | {
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "step.started" readonly type: "session.step.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -4474,7 +4596,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "step.ended" readonly type: "session.step.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -4496,7 +4618,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "step.failed" readonly type: "session.step.failed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -4509,7 +4631,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "text.started" readonly type: "session.text.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly assistantMessageID: string; readonly textID: string } readonly data: { readonly sessionID: string; readonly assistantMessageID: string; readonly textID: string }
@@ -4518,7 +4640,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "text.delta" readonly type: "session.text.delta"
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
readonly sessionID: string readonly sessionID: string
@@ -4531,7 +4653,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "text.ended" readonly type: "session.text.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -4545,7 +4667,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "reasoning.started" readonly type: "session.reasoning.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -4559,7 +4681,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "reasoning.delta" readonly type: "session.reasoning.delta"
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
readonly sessionID: string readonly sessionID: string
@@ -4572,7 +4694,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "reasoning.ended" readonly type: "session.reasoning.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -4587,7 +4709,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "tool.input.started" readonly type: "session.tool.input.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -4601,7 +4723,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "tool.input.delta" readonly type: "session.tool.input.delta"
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
readonly sessionID: string readonly sessionID: string
@@ -4614,7 +4736,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "tool.input.ended" readonly type: "session.tool.input.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -4628,7 +4750,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "tool.called" readonly type: "session.tool.called"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -4647,7 +4769,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "tool.progress" readonly type: "session.tool.progress"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -4665,7 +4787,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "tool.success" readonly type: "session.tool.success"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -4689,7 +4811,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "tool.failed" readonly type: "session.tool.failed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -4708,7 +4830,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "retried" readonly type: "session.retried"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -4728,7 +4850,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "compaction.started" readonly type: "session.compaction.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly reason: "auto" | "manual" } readonly data: { readonly sessionID: string; readonly reason: "auto" | "manual" }
@@ -4737,7 +4859,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "compaction.delta" readonly type: "session.compaction.delta"
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly text: string } readonly data: { readonly sessionID: string; readonly text: string }
} }
@@ -4745,7 +4867,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "compaction.ended" readonly type: "session.compaction.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -4759,7 +4881,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "revert.staged" readonly type: "session.revert.staged"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly data: {
@@ -4783,7 +4905,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "revert.cleared" readonly type: "session.revert.cleared"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string } readonly data: { readonly sessionID: string }
@@ -4792,7 +4914,7 @@ export type EventSubscribeOutput =
readonly id: string readonly id: string
readonly created: number readonly created: number
readonly metadata?: { readonly [x: string]: unknown } readonly metadata?: { readonly [x: string]: unknown }
readonly type: "revert.committed" readonly type: "session.revert.committed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number } readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly messageID: string } readonly data: { readonly sessionID: string; readonly messageID: string }
+5 -5
View File
@@ -45,9 +45,9 @@ test("event.subscribe exposes and decodes the native Effect event stream", async
return yield* client.event.subscribe().pipe(Stream.runCollect) return yield* client.event.subscribe().pipe(Stream.runCollect)
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise) }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
expect(Array.from(events).map((event) => event.type)).toEqual(["server.connected", "model.selected"]) expect(Array.from(events).map((event) => event.type)).toEqual(["server.connected", "session.model.selected"])
const durable = events[1] const durable = events[1]
if (durable?.type !== "model.selected") throw new Error("Expected model event") if (durable?.type !== "session.model.selected") throw new Error("Expected model event")
expect(DateTime.toEpochMillis(durable.created)).toBe(1_717_171_717_000) expect(DateTime.toEpochMillis(durable.created)).toBe(1_717_171_717_000)
expect(durable.durable).toEqual({ aggregateID: "ses_test", seq: 1, version: 1 }) expect(durable.durable).toEqual({ aggregateID: "ses_test", seq: 1, version: 1 })
}) })
@@ -159,8 +159,8 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
expect(result.context).toEqual([]) expect(result.context).toEqual([])
expect(logQueries[0]).toEqual({ after: "0" }) expect(logQueries[0]).toEqual({ after: "0" })
const logged = Array.from(result.log) const logged = Array.from(result.log)
expect(logged.map((item) => item.type)).toEqual(["model.selected", "log.synced"]) expect(logged.map((item) => item.type)).toEqual(["session.model.selected", "log.synced"])
expect(logged[0]?.type === "model.selected" && DateTime.toEpochMillis(logged[0].created)).toBe( expect(logged[0]?.type === "session.model.selected" && DateTime.toEpochMillis(logged[0].created)).toBe(
1_717_171_717_000, 1_717_171_717_000,
) )
expect(logged.at(-1)).toEqual(synced) expect(logged.at(-1)).toEqual(synced)
@@ -228,7 +228,7 @@ const modelSwitchedMessage = {
const modelSwitchedEvent = { const modelSwitchedEvent = {
id: "evt_model", id: "evt_model",
created: 1_717_171_717_000, created: 1_717_171_717_000,
type: "model.selected", type: "session.model.selected",
durable: { aggregateID: "ses_test", seq: 1, version: 1 }, durable: { aggregateID: "ses_test", seq: 1, version: 1 },
data: { data: {
sessionID: "ses_test", sessionID: "ses_test",
+2 -2
View File
@@ -160,7 +160,7 @@ test("event.subscribe exposes the Promise event stream wire projection", async (
for await (const event of client.event.subscribe()) events.push(event) for await (const event of client.event.subscribe()) events.push(event)
expect(events).toEqual([{ id: "evt_connected", created: 0, type: "server.connected", data: {} }, modelSwitchedEvent]) expect(events).toEqual([{ id: "evt_connected", created: 0, type: "server.connected", data: {} }, modelSwitchedEvent])
expect(events[1]?.type === "model.selected" && events[1].created).toBe(1_717_171_717_000) expect(events[1]?.type === "session.model.selected" && events[1].created).toBe(1_717_171_717_000)
}) })
test("event.subscribe terminates on malformed Promise SSE data", async () => { test("event.subscribe terminates on malformed Promise SSE data", async () => {
@@ -329,7 +329,7 @@ const synced = { type: "log.synced", aggregateID: "ses_test", seq: 1 }
const modelSwitchedEvent = { const modelSwitchedEvent = {
id: "evt_model", id: "evt_model",
created: 1_717_171_717_000, created: 1_717_171_717_000,
type: "model.selected", type: "session.model.selected",
durable: { aggregateID: "ses_test", seq: 1, version: 1 }, durable: { aggregateID: "ses_test", seq: 1, version: 1 },
data: { data: {
sessionID: "ses_test", sessionID: "ses_test",
+1
View File
@@ -43,5 +43,6 @@ export const migrations = (
import("./migration/20260702134641_add_session_context_entry"), import("./migration/20260702134641_add_session_context_entry"),
import("./migration/20260703090000_reset_v2_event_rename_sweep"), import("./migration/20260703090000_reset_v2_event_rename_sweep"),
import("./migration/20260703181610_event_created_column"), import("./migration/20260703181610_event_created_column"),
import("./migration/20260703190000_reset_v2_shell_event_payloads"),
]) ])
).map((module) => module.default) satisfies DatabaseMigration.Migration[] ).map((module) => module.default) satisfies DatabaseMigration.Migration[]
@@ -0,0 +1,14 @@
import { Effect } from "effect"
import type { DatabaseMigration } from "../migration"
export default {
id: "20260703190000_reset_v2_shell_event_payloads",
up(tx) {
return Effect.gen(function* () {
yield* tx.run(`DELETE FROM \`session_input\`;`)
yield* tx.run(`DELETE FROM \`session_message\`;`)
yield* tx.run(`DELETE FROM \`event\`;`)
yield* tx.run(`DELETE FROM \`event_sequence\`;`)
})
},
} satisfies DatabaseMigration.Migration
+44 -22
View File
@@ -41,8 +41,8 @@ import type { EventLog } from "@opencode-ai/schema/event-log"
import { SkillV2 } from "./skill" import { SkillV2 } from "./skill"
import { Job } from "./job" import { Job } from "./job"
import { CommandV2 } from "./command" import { CommandV2 } from "./command"
import { Identifier } from "./util/identifier"
import { Shell } from "./shell" import { Shell } from "./shell"
import { Shell as ShellSchema } from "@opencode-ai/schema/shell"
import { KeyedMutex } from "./effect/keyed-mutex" import { KeyedMutex } from "./effect/keyed-mutex"
export const RevertState = Revert.State export const RevertState = Revert.State
@@ -272,19 +272,6 @@ const layer = Layer.effect(
), ),
) )
// Session shell is user-initiated and synchronous at the API boundary, while
// the Location shell service owns process lifecycle and file-backed output.
const runShellCommand = (command: string, cwd: string) =>
Effect.gen(function* () {
const shell = yield* Shell.Service
const info = yield* shell.create({ command, cwd })
yield* shell.wait(info.id)
const output = yield* shell.output(info.id, { limit: SHELL_MAX_CAPTURE_BYTES })
return output.output || "(no output)"
}).pipe(
Effect.catchTag("Shell.NotFoundError", () => Effect.succeed("Shell command output is no longer available.")),
)
const result = Service.of({ const result = Service.of({
create: Effect.fn("V2Session.create")(function* (input) { create: Effect.fn("V2Session.create")(function* (input) {
const sessionID = input.id ?? SessionSchema.ID.create() const sessionID = input.id ?? SessionSchema.ID.create()
@@ -550,23 +537,38 @@ const layer = Layer.effect(
Effect.gen(function* () { Effect.gen(function* () {
activeShells.add(input.sessionID) activeShells.add(input.sessionID)
if ((yield* execution.active).has(input.sessionID)) yield* execution.awaitIdle(input.sessionID) if ((yield* execution.active).has(input.sessionID)) yield* execution.awaitIdle(input.sessionID)
const callID = Identifier.ascending() const started = yield* Effect.gen(function* () {
const shell = yield* Shell.Service
return yield* shell.create({ command: input.command, cwd: session.location.directory })
}).pipe(Effect.provide(locations.get(session.location)))
yield* events.publish( yield* events.publish(
SessionEvent.Shell.Started, SessionEvent.Shell.Started,
{ {
sessionID: input.sessionID, sessionID: input.sessionID,
callID, shell: started,
command: input.command,
}, },
{ id: input.id }, { id: input.id },
) )
const output = yield* runShellCommand(input.command, session.location.directory).pipe( const completed = yield* Effect.gen(function* () {
Effect.provide(locations.get(session.location)), const shell = yield* Shell.Service
) const terminal = yield* shell.wait(started.id).pipe(
Effect.map((info) => ({ info, retained: true as const })),
Effect.catchTag("Shell.NotFoundError", () =>
Effect.succeed({ info: synthesizeTerminalShellInfo(started), retained: false as const }),
),
)
const output = terminal.retained
? yield* shell
.output(started.id, { limit: SHELL_MAX_CAPTURE_BYTES })
.pipe(Effect.catchTag("Shell.NotFoundError", () => Effect.succeed(missingShellOutput())))
: missingShellOutput()
return { shell: terminal.info, output }
})
.pipe(Effect.provide(locations.get(session.location)))
yield* events.publish(SessionEvent.Shell.Ended, { yield* events.publish(SessionEvent.Shell.Ended, {
sessionID: input.sessionID, sessionID: input.sessionID,
callID, shell: completed.shell,
output, output: completed.output,
}) })
}).pipe( }).pipe(
Effect.ensuring( Effect.ensuring(
@@ -706,6 +708,26 @@ const layer = Layer.effect(
}), }),
) )
function missingShellOutput() {
const output = "Shell command output is no longer available."
return {
output,
cursor: Buffer.byteLength(output),
size: Buffer.byteLength(output),
truncated: false,
}
}
function synthesizeTerminalShellInfo(started: ShellSchema.Info): ShellSchema.Info {
return {
...started,
// The Shell record was removed before waiters could observe it; publish a terminal
// boundary instead of leaving the Session shell message permanently running.
status: "killed",
time: { ...started.time, completed: Date.now() },
}
}
const resolvePrompt = (input: PromptInput.Prompt) => const resolvePrompt = (input: PromptInput.Prompt) =>
Prompt.make({ Prompt.make({
text: input.text, text: input.text,
+1 -1
View File
@@ -129,7 +129,7 @@ const serialize = (message: SessionMessage.Message) => {
if (message.type === "system") return `[System update]: ${message.text}` if (message.type === "system") return `[System update]: ${message.text}`
if (message.type === "synthetic") return `[Synthetic context]: ${message.text}` if (message.type === "synthetic") return `[Synthetic context]: ${message.text}`
if (message.type === "skill") return `[Skill activated: ${message.name}]\n${message.text}` if (message.type === "skill") return `[Skill activated: ${message.name}]\n${message.text}`
if (message.type === "shell") return `[Shell]: ${message.command}\n${truncate(message.output)}` if (message.type === "shell") return `[Shell]: ${message.shell.command}\n${truncate(message.output?.output ?? "")}`
return "" return ""
} }
+54 -52
View File
@@ -8,21 +8,25 @@ export type MemoryState = {
} }
export interface Adapter { export interface Adapter {
readonly getCurrentAssistant: () => Effect.Effect<SessionMessage.Assistant | undefined> readonly getCurrentAssistant: () => Effect.Effect<SessionMessage.Assistant | undefined, never, never>
readonly getAssistant: (messageID: SessionMessage.ID) => Effect.Effect<SessionMessage.Assistant | undefined> readonly getAssistant: (
readonly getCurrentShell: (callID: string) => Effect.Effect<SessionMessage.Shell | undefined> messageID: SessionMessage.ID,
readonly updateAssistant: (assistant: SessionMessage.Assistant) => Effect.Effect<void> ) => Effect.Effect<SessionMessage.Assistant | undefined, never, never>
readonly updateShell: (shell: SessionMessage.Shell) => Effect.Effect<void> readonly getShell: (
readonly appendMessage: (message: SessionMessage.Message) => Effect.Effect<void> shellID: SessionMessage.Shell["shell"]["id"],
) => Effect.Effect<SessionMessage.Shell | undefined, never, never>
readonly updateAssistant: (assistant: SessionMessage.Assistant) => Effect.Effect<void, never, never>
readonly updateShell: (shell: SessionMessage.Shell) => Effect.Effect<void, never, never>
readonly appendMessage: (message: SessionMessage.Message) => Effect.Effect<void, never, never>
} }
export function memory(state: MemoryState): Adapter { export function memory(state: MemoryState): Adapter {
const assistantIndex = (messageID: SessionMessage.ID) => const assistantIndex = (messageID: SessionMessage.ID) =>
state.messages.findLastIndex((message) => message.id === messageID) state.messages.findLastIndex((message) => message.id === messageID)
const shellIndex = (messageID: SessionMessage.ID) =>
state.messages.findLastIndex((message) => message.id === messageID)
// A newer step supersedes stale incomplete rows; never resume an older assistant projection. // A newer step supersedes stale incomplete rows; never resume an older assistant projection.
const latestAssistantIndex = () => state.messages.findLastIndex((message) => message.type === "assistant") const latestAssistantIndex = () => state.messages.findLastIndex((message) => message.type === "assistant")
const activeShellIndex = (callID: string) =>
state.messages.findLastIndex((message) => message.type === "shell" && message.callID === callID)
return { return {
getCurrentAssistant() { getCurrentAssistant() {
@@ -41,12 +45,11 @@ export function memory(state: MemoryState): Adapter {
return assistant?.type === "assistant" ? assistant : undefined return assistant?.type === "assistant" ? assistant : undefined
}) })
}, },
getCurrentShell(callID) { getShell(shellID) {
return Effect.sync(() => { return Effect.sync(() => {
const index = activeShellIndex(callID) return state.messages.find((message): message is SessionMessage.Shell => {
if (index < 0) return return message.type === "shell" && message.shell.id === shellID
const shell = state.messages[index] })
return shell?.type === "shell" ? shell : undefined
}) })
}, },
updateAssistant(assistant) { updateAssistant(assistant) {
@@ -60,7 +63,7 @@ export function memory(state: MemoryState): Adapter {
}, },
updateShell(shell) { updateShell(shell) {
return Effect.sync(() => { return Effect.sync(() => {
const index = activeShellIndex(shell.callID) const index = shellIndex(shell.id)
if (index < 0) return if (index < 0) return
const current = state.messages[index] const current = state.messages[index]
if (current?.type !== "shell") return if (current?.type !== "shell") return
@@ -100,7 +103,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
return Effect.gen(function* () { return Effect.gen(function* () {
yield* SessionEvent.All.match(event, { yield* SessionEvent.All.match(event, {
"agent.selected": (event) => { "session.agent.selected": (event) => {
return adapter.appendMessage( return adapter.appendMessage(
SessionMessage.AgentSelected.make({ SessionMessage.AgentSelected.make({
id: SessionMessage.ID.fromEvent(event.id), id: SessionMessage.ID.fromEvent(event.id),
@@ -111,7 +114,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
}), }),
) )
}, },
"model.selected": (event) => { "session.model.selected": (event) => {
return adapter.appendMessage( return adapter.appendMessage(
SessionMessage.ModelSelected.make({ SessionMessage.ModelSelected.make({
id: SessionMessage.ID.fromEvent(event.id), id: SessionMessage.ID.fromEvent(event.id),
@@ -123,11 +126,11 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
) )
}, },
"session.moved": () => Effect.void, "session.moved": () => Effect.void,
renamed: () => Effect.void, "session.renamed": () => Effect.void,
forked: () => Effect.void, "session.forked": () => Effect.void,
"prompt.promoted": () => Effect.void, "session.prompt.promoted": () => Effect.void,
"prompt.admitted": () => Effect.void, "session.prompt.admitted": () => Effect.void,
"execution.settled": () => Effect.void, "session.execution.settled": () => Effect.void,
"session.context.updated": (event) => "session.context.updated": (event) =>
adapter.appendMessage( adapter.appendMessage(
SessionMessage.System.make({ SessionMessage.System.make({
@@ -137,7 +140,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
time: { created: event.created }, time: { created: event.created },
}), }),
), ),
synthetic: (event) => { "session.synthetic": (event) => {
return adapter.appendMessage( return adapter.appendMessage(
SessionMessage.Synthetic.make({ SessionMessage.Synthetic.make({
sessionID: event.data.sessionID, sessionID: event.data.sessionID,
@@ -150,7 +153,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
}), }),
) )
}, },
"skill.activated": (event) => { "session.skill.activated": (event) => {
return adapter.appendMessage( return adapter.appendMessage(
SessionMessage.Skill.make({ SessionMessage.Skill.make({
id: SessionMessage.ID.fromEvent(event.id), id: SessionMessage.ID.fromEvent(event.id),
@@ -161,25 +164,24 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
}), }),
) )
}, },
"shell.started": (event) => { "session.shell.started": (event) => {
return adapter.appendMessage( return adapter.appendMessage(
SessionMessage.Shell.make({ SessionMessage.Shell.make({
id: SessionMessage.ID.fromEvent(event.id), id: SessionMessage.ID.fromEvent(event.id),
type: "shell", type: "shell",
metadata: event.metadata, metadata: event.metadata,
callID: event.data.callID, shell: event.data.shell,
command: event.data.command,
output: "",
time: { created: event.created }, time: { created: event.created },
}), }),
) )
}, },
"shell.ended": (event) => { "session.shell.ended": (event) => {
return Effect.gen(function* () { return Effect.gen(function* () {
const currentShell = yield* adapter.getCurrentShell(event.data.callID) const currentShell = yield* adapter.getShell(event.data.shell.id)
if (currentShell) { if (currentShell) {
yield* adapter.updateShell( yield* adapter.updateShell(
produce(currentShell, (draft) => { produce(currentShell, (draft) => {
draft.shell = castDraft(event.data.shell)
draft.output = event.data.output draft.output = event.data.output
draft.time.completed = event.created draft.time.completed = event.created
}), }),
@@ -187,7 +189,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
} }
}) })
}, },
"step.started": (event) => { "session.step.started": (event) => {
return Effect.gen(function* () { return Effect.gen(function* () {
const currentAssistant = yield* adapter.getCurrentAssistant() const currentAssistant = yield* adapter.getCurrentAssistant()
if (currentAssistant) { if (currentAssistant) {
@@ -210,7 +212,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
) )
}) })
}, },
"step.ended": (event) => { "session.step.ended": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
draft.time.completed = event.created draft.time.completed = event.created
draft.finish = event.data.finish draft.finish = event.data.finish
@@ -224,33 +226,33 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
} }
}) })
}, },
"step.failed": (event) => { "session.step.failed": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
draft.time.completed = event.created draft.time.completed = event.created
draft.finish = "error" draft.finish = "error"
draft.error = event.data.error draft.error = event.data.error
}) })
}, },
"text.started": (event) => { "session.text.started": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
draft.content.push( draft.content.push(
castDraft(SessionMessage.AssistantText.make({ type: "text", id: event.data.textID, text: "" })), castDraft(SessionMessage.AssistantText.make({ type: "text", id: event.data.textID, text: "" })),
) )
}) })
}, },
"text.delta": (event) => { "session.text.delta": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
const match = latestText(draft, event.data.textID) const match = latestText(draft, event.data.textID)
if (match) match.text += event.data.delta if (match) match.text += event.data.delta
}) })
}, },
"text.ended": (event) => { "session.text.ended": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
const match = latestText(draft, event.data.textID) const match = latestText(draft, event.data.textID)
if (match) match.text = event.data.text if (match) match.text = event.data.text
}) })
}, },
"tool.input.started": (event) => { "session.tool.input.started": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
draft.content.push( draft.content.push(
castDraft( castDraft(
@@ -265,14 +267,14 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
) )
}) })
}, },
"tool.input.delta": () => Effect.void, "session.tool.input.delta": () => Effect.void,
"tool.input.ended": (event) => { "session.tool.input.ended": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
const match = latestTool(draft, event.data.callID) const match = latestTool(draft, event.data.callID)
if (match && match.state.status === "pending") match.state.input = event.data.text if (match && match.state.status === "pending") match.state.input = event.data.text
}) })
}, },
"tool.called": (event) => { "session.tool.called": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
const match = latestTool(draft, event.data.callID) const match = latestTool(draft, event.data.callID)
if (match) { if (match) {
@@ -289,7 +291,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
} }
}) })
}, },
"tool.progress": (event) => { "session.tool.progress": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
const match = latestTool(draft, event.data.callID) const match = latestTool(draft, event.data.callID)
if (match && match.state.status === "running") { if (match && match.state.status === "running") {
@@ -298,7 +300,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
} }
}) })
}, },
"tool.success": (event) => { "session.tool.success": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
const match = latestTool(draft, event.data.callID) const match = latestTool(draft, event.data.callID)
if (match && match.state.status === "running") { if (match && match.state.status === "running") {
@@ -321,7 +323,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
} }
}) })
}, },
"tool.failed": (event) => { "session.tool.failed": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
const match = latestTool(draft, event.data.callID) const match = latestTool(draft, event.data.callID)
if (match && (match.state.status === "pending" || match.state.status === "running")) { if (match && (match.state.status === "pending" || match.state.status === "running")) {
@@ -344,7 +346,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
} }
}) })
}, },
"reasoning.started": (event) => { "session.reasoning.started": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
draft.content.push( draft.content.push(
castDraft( castDraft(
@@ -359,13 +361,13 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
) )
}) })
}, },
"reasoning.delta": (event) => { "session.reasoning.delta": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
const match = latestReasoning(draft, event.data.reasoningID) const match = latestReasoning(draft, event.data.reasoningID)
if (match) match.text += event.data.delta if (match) match.text += event.data.delta
}) })
}, },
"reasoning.ended": (event) => { "session.reasoning.ended": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
const match = latestReasoning(draft, event.data.reasoningID) const match = latestReasoning(draft, event.data.reasoningID)
if (match) { if (match) {
@@ -375,10 +377,10 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
} }
}) })
}, },
retried: () => Effect.void, "session.retried": () => Effect.void,
"compaction.started": () => Effect.void, "session.compaction.started": () => Effect.void,
"compaction.delta": () => Effect.void, "session.compaction.delta": () => Effect.void,
"compaction.ended": (event) => { "session.compaction.ended": (event) => {
return adapter.appendMessage( return adapter.appendMessage(
SessionMessage.Compaction.make({ SessionMessage.Compaction.make({
id: SessionMessage.ID.fromEvent(event.id), id: SessionMessage.ID.fromEvent(event.id),
@@ -391,9 +393,9 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
}), }),
) )
}, },
"revert.staged": () => Effect.void, "session.revert.staged": () => Effect.void,
"revert.cleared": () => Effect.void, "session.revert.cleared": () => Effect.void,
"revert.committed": () => Effect.void, "session.revert.committed": () => Effect.void,
}) })
}) })
} }
+14 -7
View File
@@ -393,18 +393,25 @@ function run(db: DatabaseService, event: MessageEvent) {
return message.type === "assistant" ? message : undefined return message.type === "assistant" ? message : undefined
}) })
}, },
getCurrentShell(callID) { getShell(shellID) {
return Effect.gen(function* () { return Effect.gen(function* () {
const rows = yield* db const row = yield* db
.select() .select()
.from(SessionMessageTable) .from(SessionMessageTable)
.where(and(eq(SessionMessageTable.session_id, event.data.sessionID), eq(SessionMessageTable.type, "shell"))) .where(
and(
eq(SessionMessageTable.session_id, event.data.sessionID),
eq(SessionMessageTable.type, "shell"),
sql`json_extract(${SessionMessageTable.data}, '$.shell.id') = ${shellID}`,
),
)
.orderBy(desc(SessionMessageTable.seq)) .orderBy(desc(SessionMessageTable.seq))
.all() .limit(1)
.get()
.pipe(Effect.orDie) .pipe(Effect.orDie)
return rows if (!row) return
.map(decodeRow) const message = decodeRow(row)
.find((message): message is SessionMessage.Shell => message.type === "shell" && message.callID === callID) return message.type === "shell" ? message : undefined
}) })
}, },
updateAssistant: updateMessage, updateAssistant: updateMessage,
@@ -139,7 +139,7 @@ function toLLMMessage(message: SessionMessage.Message, model: Model): Message[]
Message.make({ Message.make({
id: message.id, id: message.id,
role: "user", role: "user",
content: `Shell command: ${message.command}\n\n${message.output}`, content: `Shell command: ${message.shell.command}\n\n${message.output?.output ?? ""}`,
metadata: message.metadata, metadata: message.metadata,
}), }),
] ]
+10 -8
View File
@@ -213,7 +213,7 @@ describe("SessionV2.create", () => {
expect(forkContext.map((message) => message.id)).not.toEqual(parentContext.map((message) => message.id)) expect(forkContext.map((message) => message.id)).not.toEqual(parentContext.map((message) => message.id))
expect(history).toHaveLength(1) expect(history).toHaveLength(1)
expect(history[0]).toMatchObject({ expect(history[0]).toMatchObject({
type: "forked", type: "session.forked",
durable: { seq: 0 }, durable: { seq: 0 },
data: { sessionID: forked.id, parentID: parent.id }, data: { sessionID: forked.id, parentID: parent.id },
}) })
@@ -378,8 +378,8 @@ describe("SessionV2.create", () => {
expect( expect(
Array.from(yield* logEvents(session, created.id, true).pipe(Stream.take(2), Stream.runCollect)), Array.from(yield* logEvents(session, created.id, true).pipe(Stream.take(2), Stream.runCollect)),
).toMatchObject([ ).toMatchObject([
{ durable: { seq: 1 }, type: "prompt.admitted", data: { prompt: { text: "Hello" } } }, { durable: { seq: 1 }, type: "session.prompt.admitted", data: { prompt: { text: "Hello" } } },
{ durable: { seq: 2 }, type: "prompt.promoted" }, { durable: { seq: 2 }, type: "session.prompt.promoted" },
]) ])
}), }),
) )
@@ -494,8 +494,9 @@ describe("SessionV2.create", () => {
const messages = yield* session.messages({ sessionID: created.id, order: "asc" }) const messages = yield* session.messages({ sessionID: created.id, order: "asc" })
const shell = messages.find((message): message is SessionMessage.Shell => message.type === "shell") const shell = messages.find((message): message is SessionMessage.Shell => message.type === "shell")
expect(shell).toMatchObject({ type: "shell", command: "echo hello" }) expect(shell).toMatchObject({ type: "shell", shell: { command: "echo hello", status: "exited", exit: 0 } })
expect(shell?.output).toContain("hello") expect(shell?.output?.output).toContain("hello")
expect(shell?.output?.truncated).toBe(false)
expect(shell?.time.completed).toBeDefined() expect(shell?.time.completed).toBeDefined()
}), }),
), ),
@@ -513,7 +514,8 @@ describe("SessionV2.create", () => {
const messages = yield* session.messages({ sessionID: created.id, order: "asc" }) const messages = yield* session.messages({ sessionID: created.id, order: "asc" })
const shell = messages.find((message): message is SessionMessage.Shell => message.type === "shell") const shell = messages.find((message): message is SessionMessage.Shell => message.type === "shell")
expect(shell).toMatchObject({ type: "shell", command: "false" }) expect(shell).toMatchObject({ type: "shell", shell: { command: "false", status: "exited" } })
expect(shell?.shell.exit).not.toBe(0)
expect(shell?.time.completed).toBeDefined() expect(shell?.time.completed).toBeDefined()
}), }),
), ),
@@ -529,7 +531,7 @@ describe("SessionV2.create", () => {
expect(yield* session.get(created.id)).toMatchObject({ agent: "plan" }) expect(yield* session.get(created.id)).toMatchObject({ agent: "plan" })
expect( expect(
Array.from(yield* logEvents(session, created.id, true).pipe(Stream.take(1), Stream.runCollect)), Array.from(yield* logEvents(session, created.id, true).pipe(Stream.take(1), Stream.runCollect)),
).toMatchObject([{ type: "agent.selected", data: { agent: "plan" } }]) ).toMatchObject([{ type: "session.agent.selected", data: { agent: "plan" } }])
}), }),
) )
@@ -562,7 +564,7 @@ describe("SessionV2.create", () => {
expect(yield* session.get(created.id)).toMatchObject({ model }) expect(yield* session.get(created.id)).toMatchObject({ model })
expect( expect(
Array.from(yield* logEvents(session, created.id, true).pipe(Stream.take(1), Stream.runCollect)), Array.from(yield* logEvents(session, created.id, true).pipe(Stream.take(1), Stream.runCollect)),
).toMatchObject([{ type: "model.selected", data: { model } }]) ).toMatchObject([{ type: "session.model.selected", data: { model } }])
}), }),
) )
+4 -4
View File
@@ -40,14 +40,14 @@ describe("SessionV2.log", () => {
const session = yield* SessionV2.Service const session = yield* SessionV2.Service
const events = yield* EventV2.Service const events = yield* EventV2.Service
const created = yield* session.create({ location }) const created = yield* session.create({ location })
yield* session.rename({ sessionID: created.id, title: "renamed" }) yield* session.rename({ sessionID: created.id, title: "session.renamed" })
const items = Array.from(yield* Stream.runCollect(session.log({ sessionID: created.id }))) const items = Array.from(yield* Stream.runCollect(session.log({ sessionID: created.id })))
const watermark = (yield* events.sequences([created.id])).get(created.id) const watermark = (yield* events.sequences([created.id])).get(created.id)
// Session creation commits a non-public durable event, so the marker's // Session creation commits a non-public durable event, so the marker's
// seq covers more of the aggregate than the public events emitted. // seq covers more of the aggregate than the public events emitted.
expect(items.map((item) => item.type)).toEqual(["renamed", "log.synced"]) expect(items.map((item) => item.type)).toEqual(["session.renamed", "log.synced"])
expect(items.at(-1)).toEqual({ type: "log.synced", aggregateID: created.id, seq: watermark }) expect(items.at(-1)).toEqual({ type: "log.synced", aggregateID: created.id, seq: watermark })
}), }),
) )
@@ -64,7 +64,7 @@ describe("SessionV2.log", () => {
yield* session.rename({ sessionID: created.id, title: "renamed live" }) yield* session.rename({ sessionID: created.id, title: "renamed live" })
const items = Array.from(yield* Fiber.join(fiber)) const items = Array.from(yield* Fiber.join(fiber))
expect(items.map((item) => item.type)).toEqual(["log.synced", "renamed"]) expect(items.map((item) => item.type)).toEqual(["log.synced", "session.renamed"])
}), }),
) )
@@ -137,7 +137,7 @@ describe("SessionV2 watermarks", () => {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const first = yield* session.create({ location }) const first = yield* session.create({ location })
const second = yield* session.create({ location }) const second = yield* session.create({ location })
yield* session.rename({ sessionID: first.id, title: "renamed" }) yield* session.rename({ sessionID: first.id, title: "session.renamed" })
const page = yield* session.list() const page = yield* session.list()
const sequences = yield* events.sequences([first.id, second.id]) const sequences = yield* events.sequences([first.id, second.id])
+25 -6
View File
@@ -19,6 +19,7 @@ import { SessionMessageUpdater } from "@opencode-ai/core/session/message-updater
import { SessionProjector } from "@opencode-ai/core/session/projector" import { SessionProjector } from "@opencode-ai/core/session/projector"
import { SessionExecution } from "@opencode-ai/core/session/execution" import { SessionExecution } from "@opencode-ai/core/session/execution"
import { SessionInput } from "@opencode-ai/core/session/input" import { SessionInput } from "@opencode-ai/core/session/input"
import { Shell } from "@opencode-ai/schema/shell"
import { import {
SessionContextCheckpointTable, SessionContextCheckpointTable,
SessionInputTable, SessionInputTable,
@@ -257,15 +258,32 @@ describe("SessionProjector", () => {
}) })
yield* events.publish(SessionEvent.Shell.Started, { yield* events.publish(SessionEvent.Shell.Started, {
sessionID, sessionID,
callID: "shell-1", shell: Shell.Info.make({
command: "pwd", id: Shell.ID.make("sh_projector"),
status: "running",
command: "pwd",
cwd: "/project",
shell: "/bin/sh",
file: "/tmp/sh_projector.out",
metadata: {},
time: { started: 0 },
}),
}) })
yield* events.publish(SessionEvent.Shell.Ended, { yield* events.publish(SessionEvent.Shell.Ended, {
sessionID, sessionID,
callID: "shell-1", shell: Shell.Info.make({
output: "/project", id: Shell.ID.make("sh_projector"),
status: "exited",
command: "pwd",
cwd: "/project",
shell: "/bin/sh",
file: "/tmp/sh_projector.out",
exit: 0,
metadata: {},
time: { started: 0, completed: 1 },
}),
output: { output: "/project", cursor: 8, size: 8, truncated: false },
}) })
const compactionID = SessionMessage.ID.create()
yield* events.publish(SessionEvent.Compaction.Started, { yield* events.publish(SessionEvent.Compaction.Started, {
sessionID, sessionID,
reason: "manual", reason: "manual",
@@ -320,7 +338,8 @@ describe("SessionProjector", () => {
metadata: { source: "projector-test" }, metadata: { source: "projector-test" },
}) })
expect(messages.find((message) => message.type === "shell")).toMatchObject({ expect(messages.find((message) => message.type === "shell")).toMatchObject({
output: "/project", shell: { command: "pwd", status: "exited", exit: 0 },
output: { output: "/project", truncated: false },
time: { completed: DateTime.makeUnsafe(0) }, time: { completed: DateTime.makeUnsafe(0) },
}) })
expect(messages.find((message) => message.type === "compaction")).toMatchObject({ expect(messages.find((message) => message.type === "compaction")).toMatchObject({
+5 -5
View File
@@ -256,16 +256,16 @@ describe("SessionV2.prompt", () => {
const streamed = Array.from(yield* Fiber.join(fiber)) const streamed = Array.from(yield* Fiber.join(fiber))
expect(streamed.map((event): [number | undefined, string] => [event.durable?.seq, event.type])).toEqual([ expect(streamed.map((event): [number | undefined, string] => [event.durable?.seq, event.type])).toEqual([
[0, "prompt.admitted"], [0, "session.prompt.admitted"],
[1, "prompt.admitted"], [1, "session.prompt.admitted"],
[2, "prompt.promoted"], [2, "session.prompt.promoted"],
[3, "prompt.promoted"], [3, "session.prompt.promoted"],
]) ])
expect( expect(
Array.from( Array.from(
yield* publicEvents({ sessionID, after: streamed[0].durable?.seq }).pipe(Stream.take(1), Stream.runCollect), yield* publicEvents({ sessionID, after: streamed[0].durable?.seq }).pipe(Stream.take(1), Stream.runCollect),
).map((event): [number | undefined, string] => [event.durable?.seq, event.type]), ).map((event): [number | undefined, string] => [event.durable?.seq, event.type]),
).toEqual([[1, "prompt.admitted"]]) ).toEqual([[1, "session.prompt.admitted"]])
}), }),
) )
@@ -7,6 +7,7 @@ import { SessionMessage } from "@opencode-ai/core/session/message"
import { AgentAttachment, FileAttachment } from "@opencode-ai/core/session/prompt" import { AgentAttachment, FileAttachment } from "@opencode-ai/core/session/prompt"
import { toLLMMessages } from "@opencode-ai/core/session/runner/to-llm-message" import { toLLMMessages } from "@opencode-ai/core/session/runner/to-llm-message"
import { SessionV2 } from "@opencode-ai/core/session" import { SessionV2 } from "@opencode-ai/core/session"
import { Shell } from "@opencode-ai/schema/shell"
import { DateTime } from "effect" import { DateTime } from "effect"
const created = DateTime.makeUnsafe(0) const created = DateTime.makeUnsafe(0)
@@ -87,9 +88,18 @@ describe("toLLMMessages", () => {
SessionMessage.Shell.make({ SessionMessage.Shell.make({
id: id("shell"), id: id("shell"),
type: "shell", type: "shell",
callID: "shell-1", shell: Shell.Info.make({
command: "pwd", id: Shell.ID.make("sh_test"),
output: "/project", status: "exited",
command: "pwd",
cwd: "/project",
shell: "/bin/sh",
file: "/tmp/sh_test.out",
exit: 0,
metadata: {},
time: { started: 0, completed: 0 },
}),
output: { output: "/project", cursor: 8, size: 8, truncated: false },
time: { created, completed: created }, time: { created, completed: created },
}), }),
SessionMessage.Compaction.make({ SessionMessage.Compaction.make({
@@ -193,12 +193,12 @@ describe("SessionRunnerLLM recorded", () => {
.orderBy(EventTable.seq) .orderBy(EventTable.seq)
.all()).map((event) => event.type), .all()).map((event) => event.type),
).toEqual([ ).toEqual([
"prompt.admitted.1", "session.prompt.admitted.1",
"prompt.promoted.1", "session.prompt.promoted.1",
"step.started.1", "session.step.started.1",
"text.started.1", "session.text.started.1",
"text.ended.1", "session.text.ended.1",
"step.ended.1", "session.step.ended.1",
]) ])
}), }),
) )
@@ -76,7 +76,7 @@ test("local tool success serializes media base64 once and reconstructs from stru
await Effect.runPromise(publisher.publish(call)) await Effect.runPromise(publisher.publish(call))
await Effect.runPromise(publisher.publish(result)) await Effect.runPromise(publisher.publish(result))
const success = published.find((event) => event.type === "tool.success.1") const success = published.find((event) => event.type === "session.tool.success.1")
expect(success).toBeDefined() expect(success).toBeDefined()
const serialized = JSON.stringify(success) const serialized = JSON.stringify(success)
expect(serialized.split(base64)).toHaveLength(2) expect(serialized.split(base64)).toHaveLength(2)
@@ -94,7 +94,7 @@ test("provider-executed success retains its compatibility result", async () => {
const { published, publisher } = capture() const { published, publisher } = capture()
await Effect.runPromise(publisher.publish(LLMEvent.toolCall({ ...call, providerExecuted: true }))) await Effect.runPromise(publisher.publish(LLMEvent.toolCall({ ...call, providerExecuted: true })))
await Effect.runPromise(publisher.publish(LLMEvent.toolResult({ ...result, providerExecuted: true }))) await Effect.runPromise(publisher.publish(LLMEvent.toolResult({ ...result, providerExecuted: true })))
const success = published.find((event) => event.type === "tool.success.1") const success = published.find((event) => event.type === "session.tool.success.1")
expect(success?.data).toHaveProperty("result") expect(success?.data).toHaveProperty("result")
}) })
@@ -110,8 +110,8 @@ test("binary failure emits no success event", async () => {
}), }),
), ),
) )
expect(published.some((event) => event.type === "tool.success.1")).toBe(false) expect(published.some((event) => event.type === "session.tool.success.1")).toBe(false)
expect(published.some((event) => event.type === "tool.failed.1")).toBe(true) expect(published.some((event) => event.type === "session.tool.failed.1")).toBe(true)
}) })
test("old success event data containing result still decodes", () => { test("old success event data containing result still decodes", () => {
+3 -3
View File
@@ -2985,7 +2985,7 @@ describe("SessionRunnerLLM", () => {
{ type: "user", text: "Interrupt provider" }, { type: "user", text: "Interrupt provider" },
{ type: "assistant", finish: "error", error: { type: "unknown", message: "Step interrupted" } }, { type: "assistant", finish: "error", error: { type: "unknown", message: "Step interrupted" } },
]) ])
expect(yield* recordedEventTypes(sessionID)).toContain("step.failed.1") expect(yield* recordedEventTypes(sessionID)).toContain("session.step.failed.1")
yield* session.interrupt(sessionID) yield* session.interrupt(sessionID)
}), }),
) )
@@ -3029,8 +3029,8 @@ describe("SessionRunnerLLM", () => {
}, },
]) ])
const eventTypes = yield* recordedEventTypes(sessionID) const eventTypes = yield* recordedEventTypes(sessionID)
expect(eventTypes).toContain("step.failed.1") expect(eventTypes).toContain("session.step.failed.1")
expect(eventTypes).not.toContain("step.ended.1") expect(eventTypes).not.toContain("session.step.ended.1")
}), }),
) )
@@ -158,14 +158,14 @@ export async function runNonInteractivePrompt(input: Input) {
if (!("sessionID" in event.data) || event.data.sessionID !== input.sessionID) continue if (!("sessionID" in event.data) || event.data.sessionID !== input.sessionID) continue
const time = toMillis(event.created) const time = toMillis(event.created)
if (event.type === "prompt.promoted") { if (event.type === "session.prompt.promoted") {
if (event.data.inputID === messageID) { if (event.data.inputID === messageID) {
promoted = true promoted = true
continue continue
} }
} }
if ( if (
event.type === "execution.settled" && event.type === "session.execution.settled" &&
event.data.outcome === "interrupted" && event.data.outcome === "interrupted" &&
(interrupted || permissionRejected || questionRejected || formCancelled) (interrupted || permissionRejected || questionRejected || formCancelled)
) { ) {
@@ -173,7 +173,7 @@ export async function runNonInteractivePrompt(input: Input) {
} }
if (!promoted) continue if (!promoted) continue
if (event.type === "step.started") { if (event.type === "session.step.started") {
const part: StepStartPart = { const part: StepStartPart = {
id: partID(event.id), id: partID(event.id),
sessionID: input.sessionID, sessionID: input.sessionID,
@@ -189,11 +189,11 @@ export async function runNonInteractivePrompt(input: Input) {
continue continue
} }
if (event.type === "text.started") { if (event.type === "session.text.started") {
starts.set(event.data.textID, { id: partID(event.id), timestamp: time }) starts.set(event.data.textID, { id: partID(event.id), timestamp: time })
continue continue
} }
if (event.type === "text.ended") { if (event.type === "session.text.ended") {
const started = starts.get(event.data.textID) const started = starts.get(event.data.textID)
const part: TextPart = { const part: TextPart = {
id: started?.id ?? partID(event.id), id: started?.id ?? partID(event.id),
@@ -207,11 +207,11 @@ export async function runNonInteractivePrompt(input: Input) {
continue continue
} }
if (event.type === "reasoning.started") { if (event.type === "session.reasoning.started") {
starts.set(event.data.reasoningID, { id: partID(event.id), timestamp: time }) starts.set(event.data.reasoningID, { id: partID(event.id), timestamp: time })
continue continue
} }
if (event.type === "reasoning.ended" && input.thinking) { if (event.type === "session.reasoning.ended" && input.thinking) {
const started = starts.get(event.data.reasoningID) const started = starts.get(event.data.reasoningID)
const part: ReasoningPart = { const part: ReasoningPart = {
id: started?.id ?? partID(event.id), id: started?.id ?? partID(event.id),
@@ -236,7 +236,7 @@ export async function runNonInteractivePrompt(input: Input) {
continue continue
} }
if (event.type === "tool.input.started") { if (event.type === "session.tool.input.started") {
tools.set(event.data.callID, { tools.set(event.data.callID, {
id: partID(event.id), id: partID(event.id),
timestamp: time, timestamp: time,
@@ -246,12 +246,12 @@ export async function runNonInteractivePrompt(input: Input) {
}) })
continue continue
} }
if (event.type === "tool.input.ended") { if (event.type === "session.tool.input.ended") {
const current = tools.get(event.data.callID) const current = tools.get(event.data.callID)
if (current) current.raw = event.data.text if (current) current.raw = event.data.text
continue continue
} }
if (event.type === "tool.called") { if (event.type === "session.tool.called") {
const current = tools.get(event.data.callID) const current = tools.get(event.data.callID)
tools.set(event.data.callID, { tools.set(event.data.callID, {
id: current?.id ?? partID(event.id), id: current?.id ?? partID(event.id),
@@ -264,7 +264,7 @@ export async function runNonInteractivePrompt(input: Input) {
}) })
continue continue
} }
if (event.type === "tool.success") { if (event.type === "session.tool.success") {
const current = tools.get(event.data.callID) ?? fallbackTool(event) const current = tools.get(event.data.callID) ?? fallbackTool(event)
const part: ToolPart = { const part: ToolPart = {
id: current.id, id: current.id,
@@ -297,7 +297,7 @@ export async function runNonInteractivePrompt(input: Input) {
if (!emit("tool_use", time, { part })) await input.renderTool(part) if (!emit("tool_use", time, { part })) await input.renderTool(part)
continue continue
} }
if (event.type === "tool.failed") { if (event.type === "session.tool.failed") {
const current = tools.get(event.data.callID) ?? fallbackTool(event) const current = tools.get(event.data.callID) ?? fallbackTool(event)
const error = event.data.error.message const error = event.data.error.message
const part: ToolPart = { const part: ToolPart = {
@@ -328,7 +328,7 @@ export async function runNonInteractivePrompt(input: Input) {
continue continue
} }
if (event.type === "step.ended") { if (event.type === "session.step.ended") {
const part: StepFinishPart = { const part: StepFinishPart = {
id: partID(event.id), id: partID(event.id),
sessionID: input.sessionID, sessionID: input.sessionID,
@@ -342,14 +342,14 @@ export async function runNonInteractivePrompt(input: Input) {
emit("step_finish", time, { part }) emit("step_finish", time, { part })
continue continue
} }
if (event.type === "step.failed") { if (event.type === "session.step.failed") {
if (interrupted || permissionRejected || questionRejected || formCancelled) continue if (interrupted || permissionRejected || questionRejected || formCancelled) continue
emittedError = true emittedError = true
process.exitCode = 1 process.exitCode = 1
if (!emit("error", time, { error: event.data.error })) UI.error(event.data.error.message) if (!emit("error", time, { error: event.data.error })) UI.error(event.data.error.message)
continue continue
} }
if (event.type === "execution.settled") { if (event.type === "session.execution.settled") {
if (event.data.outcome === "failure" && !emittedError && !questionRejected && !formCancelled) { if (event.data.outcome === "failure" && !emittedError && !questionRejected && !formCancelled) {
emittedError = true emittedError = true
process.exitCode = 1 process.exitCode = 1
@@ -62,7 +62,7 @@ type SessionCommit = StreamCommit
// - sent: part ID → byte offset of last flushed text (for incremental output) // - sent: part ID → byte offset of last flushed text (for incremental output)
// - visible: part ID → rendered text for an active part after display transforms // - visible: part ID → rendered text for an active part after display transforms
// - end: part IDs whose time.end has arrived (part is finished) // - end: part IDs whose time.end has arrived (part is finished)
// - shell: shell call ID → chosen transcript source for direct shell calls // - shell: shell ID → chosen transcript source for direct shell calls
// - echo: message ID → bash outputs to strip from the next assistant chunk // - echo: message ID → bash outputs to strip from the next assistant chunk
type ShellCall = { type ShellCall = {
source: "shell" | "tool" source: "shell" | "tool"
@@ -607,12 +607,12 @@ function toolCommit(
} }
} }
function shellPartID(callID: string): string { function shellPartID(shellID: string): string {
return `shell:${callID}` return `shell:${shellID}`
} }
function claimShell(data: SessionData, callID: string, source: ShellCall["source"], command?: string): ShellCall { function claimShell(data: SessionData, shellID: string, source: ShellCall["source"], command?: string): ShellCall {
const current = data.shell.get(callID) const current = data.shell.get(shellID)
if (current) { if (current) {
if (command && !current.command) { if (command && !current.command) {
current.command = command current.command = command
@@ -625,7 +625,7 @@ function claimShell(data: SessionData, callID: string, source: ShellCall["source
source, source,
...(command ? { command } : {}), ...(command ? { command } : {}),
} satisfies ShellCall } satisfies ShellCall
data.shell.set(callID, next) data.shell.set(shellID, next)
return next return next
} }
@@ -728,37 +728,37 @@ export function reduceSessionData(input: SessionDataInput): SessionDataOutput {
const data = input.data const data = input.data
const event = input.event const event = input.event
if (event.type === "shell.started") { if (event.type === "session.shell.started") {
if (event.properties.sessionID !== input.sessionID) { if (event.properties.sessionID !== input.sessionID) {
return out(data, commits) return out(data, commits)
} }
const shell = claimShell(data, event.properties.callID, "shell", event.properties.command) const shell = claimShell(data, event.properties.shell.id, "shell", event.properties.shell.command)
if (shell.source !== "shell") { if (shell.source !== "shell") {
return out(data, commits) return out(data, commits)
} }
const partID = shellPartID(event.properties.callID) const partID = shellPartID(event.properties.shell.id)
if (data.ids.has(partID) || data.tools.has(partID)) { if (data.ids.has(partID) || data.tools.has(partID)) {
return out(data, commits, patch({ status: "running shell" })) return out(data, commits, patch({ status: "running shell" }))
} }
data.tools.add(partID) data.tools.add(partID)
commits.push(startShell(event.properties.callID, shell.command ?? event.properties.command)) commits.push(startShell(event.properties.shell.id, shell.command ?? event.properties.shell.command))
return out(data, commits, patch({ status: "running shell" })) return out(data, commits, patch({ status: "running shell" }))
} }
if (event.type === "shell.ended") { if (event.type === "session.shell.ended") {
if (event.properties.sessionID !== input.sessionID) { if (event.properties.sessionID !== input.sessionID) {
return out(data, commits) return out(data, commits)
} }
const shell = claimShell(data, event.properties.callID, "shell") const shell = claimShell(data, event.properties.shell.id, "shell")
if (shell.source !== "shell") { if (shell.source !== "shell") {
return out(data, commits) return out(data, commits)
} }
const partID = shellPartID(event.properties.callID) const partID = shellPartID(event.properties.shell.id)
const seen = data.tools.has(partID) const seen = data.tools.has(partID)
const command = shell.command ?? "" const command = shell.command ?? ""
data.tools.delete(partID) data.tools.delete(partID)
@@ -767,11 +767,11 @@ export function reduceSessionData(input: SessionDataInput): SessionDataOutput {
} }
if (!seen && command) { if (!seen && command) {
commits.push(startShell(event.properties.callID, command)) commits.push(startShell(event.properties.shell.id, command))
} }
data.ids.add(partID) data.ids.add(partID)
commits.push(doneShell(event.properties.callID, command, event.properties.output)) commits.push(doneShell(event.properties.shell.id, command, event.properties.output.output))
return out(data, commits) return out(data, commits)
} }
@@ -424,21 +424,21 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
} }
const reduce = (child: ChildState, event: V2Event) => { const reduce = (child: ChildState, event: V2Event) => {
if (event.type === "prompt.promoted") { if (event.type === "session.prompt.promoted") {
if (userFrame(child, event.data.inputID, "")) { if (userFrame(child, event.data.inputID, "")) {
touch(child, event.created) touch(child, event.created)
notifyDetail(child) notifyDetail(child)
} }
return return
} }
if (event.type === "step.started") { if (event.type === "session.step.started") {
touch(child, event.created) touch(child, event.created)
if (child.label === FALLBACK_LABEL && event.data.agent) child.label = Locale.titlecase(event.data.agent) if (child.label === FALLBACK_LABEL && event.data.agent) child.label = Locale.titlecase(event.data.agent)
if (child.status !== "running") child.status = "running" if (child.status !== "running") child.status = "running"
input.emit() input.emit()
return return
} }
if (event.type === "text.delta") { if (event.type === "session.text.delta") {
const projected = child.projectedText.get(event.data.textID) const projected = child.projectedText.get(event.data.textID)
const covered = projected?.indexOf(event.data.delta) ?? -1 const covered = projected?.indexOf(event.data.delta) ?? -1
if (projected && covered >= 0) { if (projected && covered >= 0) {
@@ -459,7 +459,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
notifyDetail(child) notifyDetail(child)
return return
} }
if (event.type === "text.ended") { if (event.type === "session.text.ended") {
child.text.set(event.data.textID, event.data.text) child.text.set(event.data.textID, event.data.text)
child.projectedText.delete(event.data.textID) child.projectedText.delete(event.data.textID)
setFrame(child, `text:${event.data.textID}`, { setFrame(child, `text:${event.data.textID}`, {
@@ -474,7 +474,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
notifyDetail(child) notifyDetail(child)
return return
} }
if (event.type === "reasoning.delta") { if (event.type === "session.reasoning.delta") {
const projected = child.projectedReasoning.get(event.data.reasoningID) const projected = child.projectedReasoning.get(event.data.reasoningID)
const covered = projected?.indexOf(event.data.delta) ?? -1 const covered = projected?.indexOf(event.data.delta) ?? -1
if (projected && covered >= 0) { if (projected && covered >= 0) {
@@ -495,7 +495,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
notifyDetail(child) notifyDetail(child)
return return
} }
if (event.type === "reasoning.ended") { if (event.type === "session.reasoning.ended") {
child.reasoning.set(event.data.reasoningID, event.data.text) child.reasoning.set(event.data.reasoningID, event.data.text)
child.projectedReasoning.delete(event.data.reasoningID) child.projectedReasoning.delete(event.data.reasoningID)
if (!input.thinking) return if (!input.thinking) return
@@ -510,11 +510,11 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
notifyDetail(child) notifyDetail(child)
return return
} }
if (event.type === "tool.input.started") { if (event.type === "session.tool.input.started") {
child.tools.set(event.data.callID, { name: event.data.name, input: {}, started: event.created }) child.tools.set(event.data.callID, { name: event.data.name, input: {}, started: event.created })
return return
} }
if (event.type === "tool.called") { if (event.type === "session.tool.called") {
const current = child.tools.get(event.data.callID) const current = child.tools.get(event.data.callID)
child.tools.set(event.data.callID, { child.tools.set(event.data.callID, {
name: event.data.tool, name: event.data.tool,
@@ -537,10 +537,10 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
notifyDetail(child) notifyDetail(child)
return return
} }
if (event.type === "tool.success" || event.type === "tool.failed") { if (event.type === "session.tool.success" || event.type === "session.tool.failed") {
if (child.finishedTools.has(event.data.callID)) return if (child.finishedTools.has(event.data.callID)) return
const current = child.tools.get(event.data.callID) const current = child.tools.get(event.data.callID)
const failed = event.type === "tool.failed" const failed = event.type === "session.tool.failed"
childTool( childTool(
child, child,
{ {
@@ -577,7 +577,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
notifyDetail(child) notifyDetail(child)
return return
} }
if (event.type === "step.failed") { if (event.type === "session.step.failed") {
setFrame(child, `error:step:${event.data.assistantMessageID}`, { setFrame(child, `error:step:${event.data.assistantMessageID}`, {
kind: "error", kind: "error",
source: "system", source: "system",
@@ -589,7 +589,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
notifyDetail(child) notifyDetail(child)
return return
} }
if (event.type === "execution.settled") { if (event.type === "session.execution.settled") {
child.status = child.status =
event.data.outcome === "success" ? "completed" : event.data.outcome === "interrupted" ? "cancelled" : "error" event.data.outcome === "success" ? "completed" : event.data.outcome === "interrupted" ? "cancelled" : "error"
touch(child, event.created) touch(child, event.created)
@@ -613,15 +613,15 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
return { return {
main(event) { main(event) {
if (event.type === "tool.called") { if (event.type === "session.tool.called") {
if (event.data.tool === "subagent") pendingCalls.set(event.data.callID, event.data.input) if (event.data.tool === "subagent") pendingCalls.set(event.data.callID, event.data.input)
return return
} }
if (event.type === "tool.failed") { if (event.type === "session.tool.failed") {
pendingCalls.delete(event.data.callID) pendingCalls.delete(event.data.callID)
return return
} }
if (event.type !== "tool.success") return if (event.type !== "session.tool.success") return
const pending = pendingCalls.get(event.data.callID) const pending = pendingCalls.get(event.data.callID)
pendingCalls.delete(event.data.callID) pendingCalls.delete(event.data.callID)
const found = childSessionID(record(event.data.structured)) const found = childSessionID(record(event.data.structured))
@@ -213,7 +213,9 @@ function promptAgents(next: SessionTurnInput) {
? [ ? [
{ {
name: part.name, name: part.name,
source: part.source ? { start: part.source.start, end: part.source.end, text: part.source.value } : undefined, source: part.source
? { start: part.source.start, end: part.source.end, text: part.source.value }
: undefined,
}, },
] ]
: [], : [],
@@ -404,27 +406,39 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
return return
} }
if (message.type === "shell") { if (message.type === "shell") {
state.shellCommands.set(message.callID, message.command) state.shellCommands.set(message.shell.id, message.shell.command)
const completed = message.time.completed !== undefined const completed = message.time.completed !== undefined
if (!render) { if (!render) {
// Suppressed history: mark settled shells rendered so live redelivery // Suppressed history: mark settled shells rendered so live redelivery
// stays silent. A still-running shell stays unmarked and renders in // stays silent. A still-running shell stays unmarked and renders in
// full when its live shell.ended event arrives. // full when its live shell.ended event arrives.
if (completed) { if (completed) {
state.shellStarted.add(message.callID) state.shellStarted.add(message.shell.id)
state.shellEnded.add(message.callID) state.shellEnded.add(message.shell.id)
} }
return return
} }
if (!state.shellStarted.has(message.callID)) { if (!state.shellStarted.has(message.shell.id)) {
state.shellStarted.add(message.callID) state.shellStarted.add(message.shell.id)
write([shellCommit(message.callID, message.command, { text: "running shell", phase: "start", toolState: "running" })]) write([
shellCommit(message.shell.id, message.shell.command, {
text: "running shell",
phase: "start",
toolState: "running",
}),
])
} }
if (completed && !state.shellEnded.has(message.callID)) { if (completed && message.output && !state.shellEnded.has(message.shell.id)) {
state.shellEnded.add(message.callID) state.shellEnded.add(message.shell.id)
write([shellCommit(message.callID, message.command, { text: message.output, phase: "progress", toolState: "completed" })]) write([
shellCommit(message.shell.id, message.shell.command, {
text: message.output.output,
phase: "progress",
toolState: "completed",
}),
])
} }
if (completed && state.shellWait?.callID === message.callID) state.shellWait.resolve() if (completed && state.shellWait?.callID === message.shell.id) state.shellWait.resolve()
return return
} }
if (message.type !== "assistant") return if (message.type !== "assistant") return
@@ -515,17 +529,17 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
} }
input.trace?.write("recv.event", event) input.trace?.write("recv.event", event)
subagents.main(event) subagents.main(event)
if (event.type === "prompt.promoted") { if (event.type === "session.prompt.promoted") {
if (state.wait?.messageID === event.data.inputID) state.wait.promoted = true if (state.wait?.messageID === event.data.inputID) state.wait.promoted = true
state.messageIDs.add(event.data.inputID) state.messageIDs.add(event.data.inputID)
write([], { phase: "running", status: "waiting for assistant" }) write([], { phase: "running", status: "waiting for assistant" })
return return
} }
if (event.type === "step.started") { if (event.type === "session.step.started") {
write([], { phase: "running", status: "assistant responding" }) write([], { phase: "running", status: "assistant responding" })
return return
} }
if (event.type === "skill.activated") { if (event.type === "session.skill.activated") {
const messageID = event.id.replace(/^evt_/, "msg_") const messageID = event.id.replace(/^evt_/, "msg_")
if (state.wait) state.wait.promoted = true if (state.wait) state.wait.promoted = true
if (state.skillMessages.has(messageID)) return if (state.skillMessages.has(messageID)) return
@@ -533,38 +547,56 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
write([skillCommit(messageID, event.data.name)]) write([skillCommit(messageID, event.data.name)])
return return
} }
if (event.type === "shell.started") { if (event.type === "session.shell.started") {
state.shellCommands.set(event.data.callID, event.data.command) state.shellCommands.set(event.data.shell.id, event.data.shell.command)
const wait = state.shellWait const wait = state.shellWait
if (wait && wait.callID === undefined) wait.callID = event.data.callID if (wait && wait.callID === undefined) wait.callID = event.data.shell.id
if (state.shellStarted.has(event.data.callID)) return if (state.shellStarted.has(event.data.shell.id)) return
state.shellStarted.add(event.data.callID) state.shellStarted.add(event.data.shell.id)
write([shellCommit(event.data.callID, event.data.command, { text: "running shell", phase: "start", toolState: "running" })], { write(
phase: "running", [
status: "running shell", shellCommit(event.data.shell.id, event.data.shell.command, {
}) text: "running shell",
phase: "start",
toolState: "running",
}),
],
{
phase: "running",
status: "running shell",
},
)
return return
} }
if (event.type === "shell.ended") { if (event.type === "session.shell.ended") {
const command = state.shellCommands.get(event.data.callID) ?? "" const command = state.shellCommands.get(event.data.shell.id) ?? event.data.shell.command
const commits: StreamCommit[] = [] const commits: StreamCommit[] = []
if (!state.shellStarted.has(event.data.callID)) { if (!state.shellStarted.has(event.data.shell.id)) {
state.shellStarted.add(event.data.callID) state.shellStarted.add(event.data.shell.id)
if (command) commits.push(shellCommit(event.data.callID, command, { text: "running shell", phase: "start", toolState: "running" })) if (command)
commits.push(
shellCommit(event.data.shell.id, command, { text: "running shell", phase: "start", toolState: "running" }),
)
} }
if (!state.shellEnded.has(event.data.callID)) { if (!state.shellEnded.has(event.data.shell.id)) {
state.shellEnded.add(event.data.callID) state.shellEnded.add(event.data.shell.id)
commits.push(shellCommit(event.data.callID, command, { text: event.data.output, phase: "progress", toolState: "completed" })) commits.push(
shellCommit(event.data.shell.id, command, {
text: event.data.output.output,
phase: "progress",
toolState: "completed",
}),
)
} }
const wait = state.shellWait const wait = state.shellWait
// An unset callID means shell.started has not been observed yet (event // An unset callID means shell.started has not been observed yet (event
// delivery lag); mini serializes its own shells, so adopt this ended. // delivery lag); mini serializes its own shells, so adopt this ended.
const owned = wait !== undefined && (wait.callID === undefined || wait.callID === event.data.callID) const owned = wait !== undefined && (wait.callID === undefined || wait.callID === event.data.shell.id)
write(commits, owned || state.wait ? undefined : { phase: "idle", status: "" }) write(commits, owned || state.wait ? undefined : { phase: "idle", status: "" })
if (owned) wait.resolve() if (owned) wait.resolve()
return return
} }
if (event.type === "text.delta") { if (event.type === "session.text.delta") {
const key = streamPartKey(event.data.assistantMessageID, event.data.textID) const key = streamPartKey(event.data.assistantMessageID, event.data.textID)
const projected = state.projectedText.get(key) const projected = state.projectedText.get(key)
const covered = projected?.indexOf(event.data.delta) ?? -1 const covered = projected?.indexOf(event.data.delta) ?? -1
@@ -586,7 +618,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
]) ])
return return
} }
if (event.type === "text.ended") { if (event.type === "session.text.ended") {
const key = streamPartKey(event.data.assistantMessageID, event.data.textID) const key = streamPartKey(event.data.assistantMessageID, event.data.textID)
const previous = state.text.get(key) ?? "" const previous = state.text.get(key) ?? ""
if (event.data.text.length > previous.length) if (event.data.text.length > previous.length)
@@ -604,7 +636,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
state.projectedText.delete(key) state.projectedText.delete(key)
return return
} }
if (event.type === "reasoning.delta") { if (event.type === "session.reasoning.delta") {
const key = streamPartKey(event.data.assistantMessageID, event.data.reasoningID) const key = streamPartKey(event.data.assistantMessageID, event.data.reasoningID)
const projected = state.projectedReasoning.get(key) const projected = state.projectedReasoning.get(key)
const covered = projected?.indexOf(event.data.delta) ?? -1 const covered = projected?.indexOf(event.data.delta) ?? -1
@@ -627,7 +659,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
]) ])
return return
} }
if (event.type === "reasoning.ended") { if (event.type === "session.reasoning.ended") {
const key = streamPartKey(event.data.assistantMessageID, event.data.reasoningID) const key = streamPartKey(event.data.assistantMessageID, event.data.reasoningID)
const previous = state.reasoning.get(key) ?? "" const previous = state.reasoning.get(key) ?? ""
if (input.thinking && event.data.text.length > previous.length) if (input.thinking && event.data.text.length > previous.length)
@@ -645,7 +677,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
state.projectedReasoning.delete(key) state.projectedReasoning.delete(key)
return return
} }
if (event.type === "tool.input.started") { if (event.type === "session.tool.input.started") {
state.tools.set(event.data.callID, { state.tools.set(event.data.callID, {
messageID: event.data.assistantMessageID, messageID: event.data.assistantMessageID,
name: event.data.name, name: event.data.name,
@@ -655,7 +687,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
}) })
return return
} }
if (event.type === "tool.called") { if (event.type === "session.tool.called") {
if (state.finishedTools.has(event.data.callID)) return if (state.finishedTools.has(event.data.callID)) return
const current = state.tools.get(event.data.callID) const current = state.tools.get(event.data.callID)
const item: SessionMessageAssistantTool = { const item: SessionMessageAssistantTool = {
@@ -669,10 +701,10 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
renderTool(event.data.assistantMessageID, item) renderTool(event.data.assistantMessageID, item)
return return
} }
if (event.type === "tool.progress") return if (event.type === "session.tool.progress") return
if (event.type === "tool.success" || event.type === "tool.failed") { if (event.type === "session.tool.success" || event.type === "session.tool.failed") {
const current = state.tools.get(event.data.callID) const current = state.tools.get(event.data.callID)
const failed = event.type === "tool.failed" const failed = event.type === "session.tool.failed"
const item: SessionMessageAssistantTool = { const item: SessionMessageAssistantTool = {
type: "tool", type: "tool",
id: event.data.callID, id: event.data.callID,
@@ -720,7 +752,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
syncBlockers() syncBlockers()
return return
} }
if (event.type === "step.ended") { if (event.type === "session.step.ended") {
const total = const total =
event.data.tokens.input + event.data.tokens.input +
event.data.tokens.output + event.data.tokens.output +
@@ -734,13 +766,13 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
}) })
return return
} }
if (event.type === "step.failed") { if (event.type === "session.step.failed") {
state.errors.add(event.data.assistantMessageID) state.errors.add(event.data.assistantMessageID)
if (state.wait) state.wait.failureRendered = true if (state.wait) state.wait.failureRendered = true
write([{ kind: "error", source: "system", text: errorMessage(event.data.error), phase: "start" }]) write([{ kind: "error", source: "system", text: errorMessage(event.data.error), phase: "start" }])
return return
} }
if (event.type === "execution.settled") { if (event.type === "session.execution.settled") {
write([], { phase: "idle", status: "" }) write([], { phase: "idle", status: "" })
const current = state.wait const current = state.wait
if (!current || (!current.promoted && !current.interrupted)) return if (!current || (!current.promoted && !current.interrupted)) return
@@ -977,10 +1009,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
sessionID: input.sessionID, sessionID: input.sessionID,
id: messageID, id: messageID,
prompt: { prompt: {
text: [ text: [next.prompt.text, ...prepared.flatMap((file) => (file.text ? [file.text] : []))].join("\n\n"),
next.prompt.text,
...prepared.flatMap((file) => (file.text ? [file.text] : [])),
].join("\n\n"),
files: attachments.length ? attachments : undefined, files: attachments.length ? attachments : undefined,
agents: agents.length ? agents : undefined, agents: agents.length ? agents : undefined,
}, },
@@ -25,7 +25,7 @@ function prompted(inputID: string): V2Event {
return { return {
id: "evt_prompted", id: "evt_prompted",
created: 0, created: 0,
type: "prompt.promoted", type: "session.prompt.promoted",
durable: { aggregateID: "ses_1", seq: 0, version: 1 }, durable: { aggregateID: "ses_1", seq: 0, version: 1 },
data: { sessionID: "ses_1", inputID }, data: { sessionID: "ses_1", inputID },
} }
@@ -35,7 +35,7 @@ function settled(outcome: "success" | "interrupted" = "success"): V2Event {
return { return {
id: "evt_settled", id: "evt_settled",
created: 0, created: 0,
type: "execution.settled", type: "session.execution.settled",
data: { sessionID: "ses_1", outcome }, data: { sessionID: "ses_1", outcome },
} }
} }
@@ -329,7 +329,7 @@ describe("run session data", () => {
test("renders direct shell mode from first-class shell events", () => { test("renders direct shell mode from first-class shell events", () => {
let data = createSessionData() let data = createSessionData()
const started = reduce(data, { const started = reduce(data, {
type: "shell.started", type: "session.shell.started",
properties: { properties: {
sessionID: "session-1", sessionID: "session-1",
timestamp: 1, timestamp: 1,
@@ -353,7 +353,7 @@ describe("run session data", () => {
data = started.data data = started.data
const ended = reduce(data, { const ended = reduce(data, {
type: "shell.ended", type: "session.shell.ended",
properties: { properties: {
sessionID: "session-1", sessionID: "session-1",
timestamp: 2, timestamp: 2,
@@ -380,7 +380,7 @@ describe("run session data", () => {
test("suppresses legacy bash part updates once shell events claim the call", () => { test("suppresses legacy bash part updates once shell events claim the call", () => {
let data = reduce(createSessionData(), { let data = reduce(createSessionData(), {
type: "shell.started", type: "session.shell.started",
properties: { properties: {
sessionID: "session-1", sessionID: "session-1",
timestamp: 1, timestamp: 1,
@@ -409,7 +409,7 @@ describe("run session data", () => {
).toEqual([]) ).toEqual([])
data = reduce(data, { data = reduce(data, {
type: "shell.ended", type: "session.shell.ended",
properties: { properties: {
sessionID: "session-1", sessionID: "session-1",
timestamp: 2, timestamp: 2,
@@ -463,7 +463,7 @@ describe("run session data", () => {
expect( expect(
reduce(data, { reduce(data, {
type: "shell.started", type: "session.shell.started",
properties: { properties: {
sessionID: "session-1", sessionID: "session-1",
timestamp: 1, timestamp: 1,
@@ -497,7 +497,7 @@ describe("run session data", () => {
expect( expect(
reduce(data, { reduce(data, {
type: "shell.ended", type: "session.shell.ended",
properties: { properties: {
sessionID: "session-1", sessionID: "session-1",
timestamp: 2, timestamp: 2,
@@ -200,7 +200,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_prompted", id: "evt_prompted",
created: 0, created: 0,
type: "prompt.promoted", type: "session.prompt.promoted",
durable: durable("ses_1"), durable: durable("ses_1"),
data: { data: {
sessionID: "ses_1", sessionID: "ses_1",
@@ -210,7 +210,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_text", id: "evt_text",
created: 0, created: 0,
type: "text.delta", type: "session.text.delta",
data: { data: {
sessionID: "ses_1", sessionID: "ses_1",
assistantMessageID: "msg_assistant", assistantMessageID: "msg_assistant",
@@ -221,7 +221,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_settled", id: "evt_settled",
created: 0, created: 0,
type: "execution.settled", type: "session.execution.settled",
data: { sessionID: "ses_1", outcome: "success" }, data: { sessionID: "ses_1", outcome: "success" },
}) })
await turn await turn
@@ -259,7 +259,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_prompted", id: "evt_prompted",
created: 0, created: 0,
type: "prompt.promoted", type: "session.prompt.promoted",
durable: durable("ses_1"), durable: durable("ses_1"),
data: { data: {
sessionID: "ses_1", sessionID: "ses_1",
@@ -269,7 +269,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_settled", id: "evt_settled",
created: 0, created: 0,
type: "execution.settled", type: "session.execution.settled",
data: { sessionID: "ses_1", outcome: "success" }, data: { sessionID: "ses_1", outcome: "success" },
}) })
}) })
@@ -353,7 +353,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_prompted", id: "evt_prompted",
created: 0, created: 0,
type: "prompt.promoted", type: "session.prompt.promoted",
durable: durable("ses_1"), durable: durable("ses_1"),
data: { data: {
sessionID: "ses_1", sessionID: "ses_1",
@@ -363,7 +363,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_settled", id: "evt_settled",
created: 0, created: 0,
type: "execution.settled", type: "session.execution.settled",
data: { sessionID: "ses_1", outcome: "success" }, data: { sessionID: "ses_1", outcome: "success" },
}) })
}) })
@@ -450,7 +450,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_prompted", id: "evt_prompted",
created: 0, created: 0,
type: "prompt.promoted", type: "session.prompt.promoted",
durable: durable("ses_1"), durable: durable("ses_1"),
data: { data: {
sessionID: "ses_1", sessionID: "ses_1",
@@ -460,7 +460,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_settled", id: "evt_settled",
created: 0, created: 0,
type: "execution.settled", type: "session.execution.settled",
data: { sessionID: "ses_1", outcome: "success" }, data: { sessionID: "ses_1", outcome: "success" },
}) })
}) })
@@ -724,7 +724,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_text", id: "evt_text",
created: 0, created: 0,
type: "text.delta", type: "session.text.delta",
data: { data: {
sessionID: "ses_1", sessionID: "ses_1",
assistantMessageID: "msg_assistant", assistantMessageID: "msg_assistant",
@@ -809,7 +809,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_reasoning", id: "evt_reasoning",
created: 0, created: 0,
type: "reasoning.ended", type: "session.reasoning.ended",
durable: durable("ses_1"), durable: durable("ses_1"),
data: { data: {
sessionID: "ses_1", sessionID: "ses_1",
@@ -865,7 +865,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_settled", id: "evt_settled",
created: 0, created: 0,
type: "execution.settled", type: "session.execution.settled",
data: { sessionID: "ses_1", outcome: "success" }, data: { sessionID: "ses_1", outcome: "success" },
}) })
await turn await turn
@@ -921,7 +921,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_prompted", id: "evt_prompted",
created: 0, created: 0,
type: "prompt.promoted", type: "session.prompt.promoted",
durable: durable("ses_1"), durable: durable("ses_1"),
data: { data: {
sessionID: "ses_1", sessionID: "ses_1",
@@ -931,7 +931,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_settled", id: "evt_settled",
created: 0, created: 0,
type: "execution.settled", type: "session.execution.settled",
data: { sessionID: "ses_1", outcome: "success" }, data: { sessionID: "ses_1", outcome: "success" },
}) })
await turn await turn
@@ -981,7 +981,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_prompted", id: "evt_prompted",
created: 0, created: 0,
type: "prompt.promoted", type: "session.prompt.promoted",
durable: durable("ses_1"), durable: durable("ses_1"),
data: { data: {
sessionID: "ses_1", sessionID: "ses_1",
@@ -993,7 +993,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_settled", id: "evt_settled",
created: 0, created: 0,
type: "execution.settled", type: "session.execution.settled",
data: { sessionID: "ses_1", outcome: "success" }, data: { sessionID: "ses_1", outcome: "success" },
}) })
await turn await turn
@@ -1021,16 +1021,42 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_shell_start", id: "evt_shell_start",
created: 0, created: 0,
type: "shell.started", type: "session.shell.started",
durable: durable("ses_1"), durable: durable("ses_1"),
data: { sessionID: "ses_1", callID: "call_shell", command: "ls" }, data: {
sessionID: "ses_1",
shell: {
id: "sh_shell",
status: "running",
command: "ls",
cwd: "/tmp",
shell: "/bin/sh",
file: "/tmp/opencode-shell",
metadata: {},
time: { started: 0 },
},
},
}) })
events.push({ events.push({
id: "evt_shell_end", id: "evt_shell_end",
created: 0, created: 0,
type: "shell.ended", type: "session.shell.ended",
durable: durable("ses_1", 1), durable: durable("ses_1", 1),
data: { sessionID: "ses_1", callID: "call_shell", output: "file.txt" }, data: {
sessionID: "ses_1",
shell: {
id: "sh_shell",
status: "exited",
command: "ls",
cwd: "/tmp",
shell: "/bin/sh",
file: "/tmp/opencode-shell",
exit: 0,
metadata: {},
time: { started: 0, completed: 1 },
},
output: { output: "file.txt", cursor: 8, size: 8, truncated: false },
},
}) })
}) })
return ok(undefined) as never return ok(undefined) as never
@@ -1047,8 +1073,8 @@ describe("V2 mini transport", () => {
expect(request).toMatchObject({ sessionID: "ses_1", command: "ls" }) expect(request).toMatchObject({ sessionID: "ses_1", command: "ls" })
expect(ui.commits.filter((item) => item.shell)).toMatchObject([ expect(ui.commits.filter((item) => item.shell)).toMatchObject([
{ phase: "start", tool: "bash", toolState: "running", shell: { callID: "call_shell", command: "ls" } }, { phase: "start", tool: "bash", toolState: "running", shell: { callID: "sh_shell", command: "ls" } },
{ phase: "progress", text: "file.txt", toolState: "completed", shell: { callID: "call_shell", command: "ls" } }, { phase: "progress", text: "file.txt", toolState: "completed", shell: { callID: "sh_shell", command: "ls" } },
]) ])
expect(ui.events).toContainEqual({ type: "stream.patch", patch: { phase: "running", status: "running shell" } }) expect(ui.events).toContainEqual({ type: "stream.patch", patch: { phase: "running", status: "running shell" } })
await transport.close() await transport.close()
@@ -1107,9 +1133,18 @@ describe("V2 mini transport", () => {
{ {
id: "msg_shell", id: "msg_shell",
type: "shell" as const, type: "shell" as const,
callID: "call_1", shell: {
command: "ls", id: "sh_1",
output: "file.txt", status: "exited",
command: "ls",
cwd: "/tmp",
shell: "/bin/sh",
file: "/tmp/opencode-shell",
exit: 0,
metadata: {},
time: { started: 0, completed: 1 },
},
output: { output: "file.txt", cursor: 8, size: 8, truncated: false },
time: { created: 1, completed: 2 }, time: { created: 1, completed: 2 },
}, },
], ],
@@ -1127,15 +1162,29 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_shell_end", id: "evt_shell_end",
created: 0, created: 0,
type: "shell.ended", type: "session.shell.ended",
durable: durable("ses_1", 1), durable: durable("ses_1", 1),
data: { sessionID: "ses_1", callID: "call_1", output: "file.txt" }, data: {
sessionID: "ses_1",
shell: {
id: "sh_1",
status: "exited",
command: "ls",
cwd: "/tmp",
shell: "/bin/sh",
file: "/tmp/opencode-shell",
exit: 0,
metadata: {},
time: { started: 0, completed: 1 },
},
output: { output: "file.txt", cursor: 8, size: 8, truncated: false },
},
}) })
await Bun.sleep(0) await Bun.sleep(0)
await Bun.sleep(0) await Bun.sleep(0)
expect(ui.commits.filter((item) => item.shell)).toMatchObject([ expect(ui.commits.filter((item) => item.shell)).toMatchObject([
{ phase: "start", shell: { callID: "call_1", command: "ls" } }, { phase: "start", shell: { callID: "sh_1", command: "ls" } },
{ phase: "progress", text: "file.txt", toolState: "completed" }, { phase: "progress", text: "file.txt", toolState: "completed" },
]) ])
await transport.close() await transport.close()
@@ -1160,7 +1209,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_prompted", id: "evt_prompted",
created: 0, created: 0,
type: "prompt.promoted", type: "session.prompt.promoted",
durable: durable("ses_1"), durable: durable("ses_1"),
data: { data: {
sessionID: "ses_1", sessionID: "ses_1",
@@ -1170,7 +1219,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_settled", id: "evt_settled",
created: 0, created: 0,
type: "execution.settled", type: "session.execution.settled",
data: { sessionID: "ses_1", outcome: "success" }, data: { sessionID: "ses_1", outcome: "success" },
}) })
}) })
@@ -1236,7 +1285,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_skill", id: "evt_skill",
created: 0, created: 0,
type: "skill.activated", type: "session.skill.activated",
durable: durable("ses_1"), durable: durable("ses_1"),
data: { data: {
sessionID: "ses_1", sessionID: "ses_1",
@@ -1247,7 +1296,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_settled", id: "evt_settled",
created: 0, created: 0,
type: "execution.settled", type: "session.execution.settled",
data: { sessionID: "ses_1", outcome: "success" }, data: { sessionID: "ses_1", outcome: "success" },
}) })
}) })
@@ -1317,7 +1366,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_unrelated_settled", id: "evt_unrelated_settled",
created: 0, created: 0,
type: "execution.settled", type: "session.execution.settled",
data: { sessionID: "ses_1", outcome: "success" }, data: { sessionID: "ses_1", outcome: "success" },
}) })
await Bun.sleep(0) await Bun.sleep(0)
@@ -1327,7 +1376,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_skill", id: "evt_skill",
created: 0, created: 0,
type: "skill.activated", type: "session.skill.activated",
durable: durable("ses_1"), durable: durable("ses_1"),
data: { data: {
sessionID: "ses_1", sessionID: "ses_1",
@@ -1338,7 +1387,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_skill_settled", id: "evt_skill_settled",
created: 0, created: 0,
type: "execution.settled", type: "session.execution.settled",
data: { sessionID: "ses_1", outcome: "success" }, data: { sessionID: "ses_1", outcome: "success" },
}) })
await turn await turn
@@ -1376,7 +1425,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_skill", id: "evt_skill",
created: 0, created: 0,
type: "skill.activated", type: "session.skill.activated",
durable: durable("ses_1"), durable: durable("ses_1"),
data: { data: {
sessionID: "ses_1", sessionID: "ses_1",
@@ -1438,7 +1487,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_child_step", id: "evt_child_step",
created: 0, created: 0,
type: "step.started", type: "session.step.started",
durable: durable("ses_child"), durable: durable("ses_child"),
data: { data: {
sessionID: "ses_child", sessionID: "ses_child",
@@ -1456,7 +1505,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_child_text", id: "evt_child_text",
created: 0, created: 0,
type: "text.delta", type: "session.text.delta",
data: { data: {
sessionID: "ses_child", sessionID: "ses_child",
assistantMessageID: "msg_child_a", assistantMessageID: "msg_child_a",
@@ -1470,7 +1519,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_child_settled", id: "evt_child_settled",
created: 0, created: 0,
type: "execution.settled", type: "session.execution.settled",
data: { sessionID: "ses_child", outcome: "success" }, data: { sessionID: "ses_child", outcome: "success" },
}) })
while (!states().some((state) => state.tabs.some((tab) => tab.status === "completed"))) await Bun.sleep(0) while (!states().some((state) => state.tabs.some((tab) => tab.status === "completed"))) await Bun.sleep(0)
@@ -1515,7 +1564,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_child_step", id: "evt_child_step",
created: 0, created: 0,
type: "step.started", type: "session.step.started",
durable: durable("ses_child"), durable: durable("ses_child"),
data: { data: {
sessionID: "ses_child", sessionID: "ses_child",
@@ -1527,7 +1576,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_child_settled", id: "evt_child_settled",
created: 0, created: 0,
type: "execution.settled", type: "session.execution.settled",
data: { sessionID: "ses_child", outcome: "interrupted" }, data: { sessionID: "ses_child", outcome: "interrupted" },
}) })
await Bun.sleep(0) await Bun.sleep(0)
@@ -1574,7 +1623,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_child_step", id: "evt_child_step",
created: 0, created: 0,
type: "step.started", type: "session.step.started",
durable: durable("ses_child"), durable: durable("ses_child"),
data: { data: {
sessionID: "ses_child", sessionID: "ses_child",
@@ -1587,7 +1636,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_parent_call", id: "evt_parent_call",
created: 0, created: 0,
type: "tool.called", type: "session.tool.called",
durable: durable("ses_1"), durable: durable("ses_1"),
data: { data: {
sessionID: "ses_1", sessionID: "ses_1",
@@ -1601,7 +1650,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_parent_success", id: "evt_parent_success",
created: 0, created: 0,
type: "tool.success", type: "session.tool.success",
durable: durable("ses_1", 1), durable: durable("ses_1", 1),
data: { data: {
sessionID: "ses_1", sessionID: "ses_1",
@@ -1616,7 +1665,7 @@ describe("V2 mini transport", () => {
events.push({ events.push({
id: "evt_child_settled", id: "evt_child_settled",
created: 0, created: 0,
type: "execution.settled", type: "session.execution.settled",
data: { sessionID: "ses_child", outcome: "interrupted" }, data: { sessionID: "ses_child", outcome: "interrupted" },
}) })
while (!states().some((state) => state.tabs.some((tab) => tab.status === "cancelled"))) await Bun.sleep(0) while (!states().some((state) => state.tabs.some((tab) => tab.status === "cancelled"))) await Bun.sleep(0)
@@ -99,10 +99,10 @@ describe("pty HttpApi bridge", () => {
const updated = await app().request(PtyPaths.update.replace(":ptyID", info.id), { const updated = await app().request(PtyPaths.update.replace(":ptyID", info.id), {
method: "PUT", method: "PUT",
headers: { ...headers, "content-type": "application/json" }, headers: { ...headers, "content-type": "application/json" },
body: JSON.stringify({ title: "renamed", size: { cols: 80, rows: 24 } }), body: JSON.stringify({ title: "session.renamed", size: { cols: 80, rows: 24 } }),
}) })
expect(updated.status).toBe(200) expect(updated.status).toBe(200)
expect(await updated.json()).toMatchObject({ id: info.id, title: "renamed" }) expect(await updated.json()).toMatchObject({ id: info.id, title: "session.renamed" })
} finally { } finally {
await app().request(PtyPaths.remove.replace(":ptyID", info.id), { method: "DELETE", headers }) await app().request(PtyPaths.remove.replace(":ptyID", info.id), { method: "DELETE", headers })
} }
@@ -573,7 +573,7 @@ describe("HttpApi SDK", () => {
const child = yield* capture(() => sdk.session.create({ title: "child", parentID })) const child = yield* capture(() => sdk.session.create({ title: "child", parentID }))
const childID = String(record(child.data).id) const childID = String(record(child.data).id)
const get = yield* capture(() => sdk.session.get({ sessionID: parentID })) const get = yield* capture(() => sdk.session.get({ sessionID: parentID }))
const update = yield* capture(() => sdk.session.update({ sessionID: parentID, title: "renamed" })) const update = yield* capture(() => sdk.session.update({ sessionID: parentID, title: "session.renamed" }))
const roots = yield* capture(() => sdk.session.list({ roots: true, limit: 10 })) const roots = yield* capture(() => sdk.session.list({ roots: true, limit: 10 }))
const all = yield* capture(() => sdk.session.list({ roots: false, limit: 10 })) const all = yield* capture(() => sdk.session.list({ roots: false, limit: 10 }))
const children = yield* capture(() => sdk.session.children({ sessionID: parentID })) const children = yield* capture(() => sdk.session.children({ sessionID: parentID }))
@@ -271,7 +271,7 @@ describe("tool.apply_patch freeform", () => {
yield* execute({ patchText }, ctx) yield* execute({ patchText }, ctx)
const moved = path.join(test.directory, "renamed", "dir", "name.txt") const moved = path.join(test.directory, "session.renamed", "dir", "name.txt")
yield* expectReadFailure(original) yield* expectReadFailure(original)
expect(yield* readText(moved)).toBe("new content\n") expect(yield* readText(moved)).toBe("new content\n")
}), }),
@@ -282,7 +282,7 @@ describe("tool.apply_patch freeform", () => {
const test = yield* TestInstance const test = yield* TestInstance
const { ctx } = makeCtx() const { ctx } = makeCtx()
const original = path.join(test.directory, "old", "name.txt") const original = path.join(test.directory, "old", "name.txt")
const destination = path.join(test.directory, "renamed", "dir", "name.txt") const destination = path.join(test.directory, "session.renamed", "dir", "name.txt")
yield* makeDir(path.dirname(original)) yield* makeDir(path.dirname(original))
yield* makeDir(path.dirname(destination)) yield* makeDir(path.dirname(destination))
yield* writeText(original, "from\n") yield* writeText(original, "from\n")
@@ -79593,8 +79593,8 @@
} }
} }
}, },
"synthetic": { "session.synthetic": {
"id": "synthetic", "id": "session.synthetic",
"env": ["SYNTHETIC_API_KEY"], "env": ["SYNTHETIC_API_KEY"],
"npm": "@ai-sdk/openai-compatible", "npm": "@ai-sdk/openai-compatible",
"api": "https://api.synthetic.new/openai/v1", "api": "https://api.synthetic.new/openai/v1",
@@ -22,7 +22,7 @@ test.skip("step snapshots carry over to assistant messages", () => {
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(), id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0), created: DateTime.makeUnsafe(0),
type: "step.started", type: "session.step.started",
durable: durable(sessionID), durable: durable(sessionID),
data: { data: {
sessionID, sessionID,
@@ -44,7 +44,7 @@ test.skip("step snapshots carry over to assistant messages", () => {
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(), id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0), created: DateTime.makeUnsafe(0),
type: "step.ended", type: "session.step.ended",
durable: durable(sessionID, 1, 2), durable: durable(sessionID, 1, 2),
data: { data: {
sessionID, sessionID,
@@ -77,7 +77,7 @@ test.skip("text ended populates assistant text content", () => {
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(), id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0), created: DateTime.makeUnsafe(0),
type: "step.started", type: "session.step.started",
durable: durable(sessionID), durable: durable(sessionID),
data: { data: {
sessionID, sessionID,
@@ -96,7 +96,7 @@ test.skip("text ended populates assistant text content", () => {
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(), id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0), created: DateTime.makeUnsafe(0),
type: "text.started", type: "session.text.started",
durable: durable(sessionID, 1), durable: durable(sessionID, 1),
data: { data: {
sessionID, sessionID,
@@ -110,7 +110,7 @@ test.skip("text ended populates assistant text content", () => {
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(), id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0), created: DateTime.makeUnsafe(0),
type: "text.ended", type: "session.text.ended",
durable: durable(sessionID, 2), durable: durable(sessionID, 2),
data: { data: {
sessionID, sessionID,
@@ -136,7 +136,7 @@ test.skip("tool completion stores completed timestamp", () => {
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(), id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0), created: DateTime.makeUnsafe(0),
type: "step.started", type: "session.step.started",
durable: durable(sessionID), durable: durable(sessionID),
data: { data: {
sessionID, sessionID,
@@ -155,7 +155,7 @@ test.skip("tool completion stores completed timestamp", () => {
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(), id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0), created: DateTime.makeUnsafe(0),
type: "tool.input.started", type: "session.tool.input.started",
durable: durable(sessionID, 1), durable: durable(sessionID, 1),
data: { data: {
sessionID, sessionID,
@@ -170,7 +170,7 @@ test.skip("tool completion stores completed timestamp", () => {
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(), id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0), created: DateTime.makeUnsafe(0),
type: "tool.called", type: "session.tool.called",
durable: durable(sessionID, 2), durable: durable(sessionID, 2),
data: { data: {
sessionID, sessionID,
@@ -187,7 +187,7 @@ test.skip("tool completion stores completed timestamp", () => {
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(), id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0), created: DateTime.makeUnsafe(0),
type: "tool.success", type: "session.tool.success",
durable: durable(sessionID, 3), durable: durable(sessionID, 3),
data: { data: {
sessionID, sessionID,
@@ -218,7 +218,7 @@ test("compaction events reduce to compaction message only when completed", () =>
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id, id,
created: DateTime.makeUnsafe(0), created: DateTime.makeUnsafe(0),
type: "compaction.started", type: "session.compaction.started",
durable: durable(sessionID), durable: durable(sessionID),
data: { data: {
sessionID, sessionID,
@@ -233,7 +233,7 @@ test("compaction events reduce to compaction message only when completed", () =>
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(), id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0), created: DateTime.makeUnsafe(0),
type: "compaction.delta", type: "session.compaction.delta",
data: { data: {
sessionID, sessionID,
text: "hello ", text: "hello ",
@@ -245,7 +245,7 @@ test("compaction events reduce to compaction message only when completed", () =>
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(), id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0), created: DateTime.makeUnsafe(0),
type: "compaction.delta", type: "session.compaction.delta",
data: { data: {
sessionID, sessionID,
text: "summary", text: "summary",
@@ -257,7 +257,7 @@ test("compaction events reduce to compaction message only when completed", () =>
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: endedID, id: endedID,
created: DateTime.makeUnsafe(0), created: DateTime.makeUnsafe(0),
type: "compaction.ended", type: "session.compaction.ended",
durable: durable(sessionID, 1), durable: durable(sessionID, 1),
data: { data: {
sessionID, sessionID,
+38 -38
View File
@@ -12,6 +12,7 @@ import { SessionID } from "./session-id.js"
import { Location } from "./location.js" import { Location } from "./location.js"
import { SessionMessage } from "./session-message.js" import { SessionMessage } from "./session-message.js"
import { Revert } from "./revert.js" import { Revert } from "./revert.js"
import { Shell as ShellSchema } from "./shell.js"
export { FileAttachment } export { FileAttachment }
@@ -51,7 +52,7 @@ export const UnknownError = SessionMessage.UnknownError
export type UnknownError = SessionMessage.UnknownError export type UnknownError = SessionMessage.UnknownError
export const AgentSelected = Event.durable({ export const AgentSelected = Event.durable({
type: "agent.selected", type: "session.agent.selected",
...options, ...options,
schema: { schema: {
...Base, ...Base,
@@ -61,7 +62,7 @@ export const AgentSelected = Event.durable({
export type AgentSelected = typeof AgentSelected.Type export type AgentSelected = typeof AgentSelected.Type
export const ModelSelected = Event.durable({ export const ModelSelected = Event.durable({
type: "model.selected", type: "session.model.selected",
...options, ...options,
schema: { schema: {
...Base, ...Base,
@@ -82,7 +83,7 @@ export const Moved = Event.durable({
export type Moved = typeof Moved.Type export type Moved = typeof Moved.Type
export const Renamed = Event.durable({ export const Renamed = Event.durable({
type: "renamed", type: "session.renamed",
...options, ...options,
schema: { schema: {
...Base, ...Base,
@@ -92,7 +93,7 @@ export const Renamed = Event.durable({
export type Renamed = typeof Renamed.Type export type Renamed = typeof Renamed.Type
export const Forked = Event.durable({ export const Forked = Event.durable({
type: "forked", type: "session.forked",
...options, ...options,
schema: { schema: {
...Base, ...Base,
@@ -103,7 +104,7 @@ export const Forked = Event.durable({
export type Forked = typeof Forked.Type export type Forked = typeof Forked.Type
export const PromptPromoted = Event.durable({ export const PromptPromoted = Event.durable({
type: "prompt.promoted", type: "session.prompt.promoted",
...options, ...options,
schema: { schema: {
sessionID: SessionID, sessionID: SessionID,
@@ -113,14 +114,14 @@ export const PromptPromoted = Event.durable({
export type PromptPromoted = typeof PromptPromoted.Type export type PromptPromoted = typeof PromptPromoted.Type
export const PromptAdmitted = Event.durable({ export const PromptAdmitted = Event.durable({
type: "prompt.admitted", type: "session.prompt.admitted",
...options, ...options,
schema: PromptFields, schema: PromptFields,
}) })
export type PromptAdmitted = typeof PromptAdmitted.Type export type PromptAdmitted = typeof PromptAdmitted.Type
export const ExecutionSettled = Event.ephemeral({ export const ExecutionSettled = Event.ephemeral({
type: "execution.settled", type: "session.execution.settled",
schema: { schema: {
...Base, ...Base,
outcome: Schema.Literals(["success", "failure", "interrupted"]), outcome: Schema.Literals(["success", "failure", "interrupted"]),
@@ -140,7 +141,7 @@ export const ContextUpdated = Event.durable({
export type ContextUpdated = typeof ContextUpdated.Type export type ContextUpdated = typeof ContextUpdated.Type
export const Synthetic = Event.durable({ export const Synthetic = Event.durable({
type: "synthetic", type: "session.synthetic",
...options, ...options,
schema: { schema: {
...Base, ...Base,
@@ -153,7 +154,7 @@ export type Synthetic = typeof Synthetic.Type
export namespace Skill { export namespace Skill {
export const Activated = Event.durable({ export const Activated = Event.durable({
type: "skill.activated", type: "session.skill.activated",
...options, ...options,
schema: { schema: {
...Base, ...Base,
@@ -166,23 +167,22 @@ export namespace Skill {
export namespace Shell { export namespace Shell {
export const Started = Event.durable({ export const Started = Event.durable({
type: "shell.started", type: "session.shell.started",
...options, ...options,
schema: { schema: {
...Base, ...Base,
callID: Schema.String, shell: ShellSchema.Info,
command: Schema.String,
}, },
}) })
export type Started = typeof Started.Type export type Started = typeof Started.Type
export const Ended = Event.durable({ export const Ended = Event.durable({
type: "shell.ended", type: "session.shell.ended",
...options, ...options,
schema: { schema: {
...Base, ...Base,
callID: Schema.String, shell: ShellSchema.Info,
output: Schema.String, output: ShellSchema.Output,
}, },
}) })
export type Ended = typeof Ended.Type export type Ended = typeof Ended.Type
@@ -190,7 +190,7 @@ export namespace Shell {
export namespace Step { export namespace Step {
export const Started = Event.durable({ export const Started = Event.durable({
type: "step.started", type: "session.step.started",
...options, ...options,
schema: { schema: {
...Base, ...Base,
@@ -203,7 +203,7 @@ export namespace Step {
export type Started = typeof Started.Type export type Started = typeof Started.Type
export const Ended = Event.durable({ export const Ended = Event.durable({
type: "step.ended", type: "session.step.ended",
...stepSettlementOptions, ...stepSettlementOptions,
schema: { schema: {
...Base, ...Base,
@@ -226,7 +226,7 @@ export namespace Step {
export type Ended = typeof Ended.Type export type Ended = typeof Ended.Type
export const Failed = Event.durable({ export const Failed = Event.durable({
type: "step.failed", type: "session.step.failed",
...stepSettlementOptions, ...stepSettlementOptions,
schema: { schema: {
...Base, ...Base,
@@ -239,7 +239,7 @@ export namespace Step {
export namespace Text { export namespace Text {
export const Started = Event.durable({ export const Started = Event.durable({
type: "text.started", type: "session.text.started",
...options, ...options,
schema: { schema: {
...Base, ...Base,
@@ -251,7 +251,7 @@ export namespace Text {
// Stream fragments are live-only; Text.Ended is the replayable full-value boundary. // Stream fragments are live-only; Text.Ended is the replayable full-value boundary.
export const Delta = Event.ephemeral({ export const Delta = Event.ephemeral({
type: "text.delta", type: "session.text.delta",
schema: { schema: {
...Base, ...Base,
assistantMessageID: SessionMessage.ID, assistantMessageID: SessionMessage.ID,
@@ -262,7 +262,7 @@ export namespace Text {
export type Delta = typeof Delta.Type export type Delta = typeof Delta.Type
export const Ended = Event.durable({ export const Ended = Event.durable({
type: "text.ended", type: "session.text.ended",
...options, ...options,
schema: { schema: {
...Base, ...Base,
@@ -276,7 +276,7 @@ export namespace Text {
export namespace Reasoning { export namespace Reasoning {
export const Started = Event.durable({ export const Started = Event.durable({
type: "reasoning.started", type: "session.reasoning.started",
...options, ...options,
schema: { schema: {
...Base, ...Base,
@@ -289,7 +289,7 @@ export namespace Reasoning {
// Stream fragments are live-only; Reasoning.Ended is the replayable full-value boundary. // Stream fragments are live-only; Reasoning.Ended is the replayable full-value boundary.
export const Delta = Event.ephemeral({ export const Delta = Event.ephemeral({
type: "reasoning.delta", type: "session.reasoning.delta",
schema: { schema: {
...Base, ...Base,
assistantMessageID: SessionMessage.ID, assistantMessageID: SessionMessage.ID,
@@ -300,7 +300,7 @@ export namespace Reasoning {
export type Delta = typeof Delta.Type export type Delta = typeof Delta.Type
export const Ended = Event.durable({ export const Ended = Event.durable({
type: "reasoning.ended", type: "session.reasoning.ended",
...options, ...options,
schema: { schema: {
...Base, ...Base,
@@ -322,7 +322,7 @@ export namespace Tool {
export namespace Input { export namespace Input {
export const Started = Event.durable({ export const Started = Event.durable({
type: "tool.input.started", type: "session.tool.input.started",
...options, ...options,
schema: { schema: {
...ToolBase, ...ToolBase,
@@ -333,7 +333,7 @@ export namespace Tool {
// Stream fragments are live-only; Input.Ended is the replayable raw-input boundary. // Stream fragments are live-only; Input.Ended is the replayable raw-input boundary.
export const Delta = Event.ephemeral({ export const Delta = Event.ephemeral({
type: "tool.input.delta", type: "session.tool.input.delta",
schema: { schema: {
...ToolBase, ...ToolBase,
delta: Schema.String, delta: Schema.String,
@@ -342,7 +342,7 @@ export namespace Tool {
export type Delta = typeof Delta.Type export type Delta = typeof Delta.Type
export const Ended = Event.durable({ export const Ended = Event.durable({
type: "tool.input.ended", type: "session.tool.input.ended",
...options, ...options,
schema: { schema: {
...ToolBase, ...ToolBase,
@@ -353,7 +353,7 @@ export namespace Tool {
} }
export const Called = Event.durable({ export const Called = Event.durable({
type: "tool.called", type: "session.tool.called",
...options, ...options,
schema: { schema: {
...ToolBase, ...ToolBase,
@@ -372,7 +372,7 @@ export namespace Tool {
* transitions or at a bounded cadence, not persist every stdout/stderr chunk. * transitions or at a bounded cadence, not persist every stdout/stderr chunk.
*/ */
export const Progress = Event.durable({ export const Progress = Event.durable({
type: "tool.progress", type: "session.tool.progress",
...options, ...options,
schema: { schema: {
...ToolBase, ...ToolBase,
@@ -383,7 +383,7 @@ export namespace Tool {
export type Progress = typeof Progress.Type export type Progress = typeof Progress.Type
export const Success = Event.durable({ export const Success = Event.durable({
type: "tool.success", type: "session.tool.success",
...options, ...options,
schema: { schema: {
...ToolBase, ...ToolBase,
@@ -400,7 +400,7 @@ export namespace Tool {
export type Success = typeof Success.Type export type Success = typeof Success.Type
export const Failed = Event.durable({ export const Failed = Event.durable({
type: "tool.failed", type: "session.tool.failed",
...options, ...options,
schema: { schema: {
...ToolBase, ...ToolBase,
@@ -428,7 +428,7 @@ export const RetryError = Schema.Struct({
export interface RetryError extends Schema.Schema.Type<typeof RetryError> {} export interface RetryError extends Schema.Schema.Type<typeof RetryError> {}
export const Retried = Event.durable({ export const Retried = Event.durable({
type: "retried", type: "session.retried",
...options, ...options,
schema: { schema: {
...Base, ...Base,
@@ -440,7 +440,7 @@ export type Retried = typeof Retried.Type
export namespace Compaction { export namespace Compaction {
export const Started = Event.durable({ export const Started = Event.durable({
type: "compaction.started", type: "session.compaction.started",
...options, ...options,
schema: { schema: {
...Base, ...Base,
@@ -450,7 +450,7 @@ export namespace Compaction {
export type Started = typeof Started.Type export type Started = typeof Started.Type
export const Delta = Event.ephemeral({ export const Delta = Event.ephemeral({
type: "compaction.delta", type: "session.compaction.delta",
schema: { schema: {
...Base, ...Base,
text: Schema.String, text: Schema.String,
@@ -459,7 +459,7 @@ export namespace Compaction {
export type Delta = typeof Delta.Type export type Delta = typeof Delta.Type
export const Ended = Event.durable({ export const Ended = Event.durable({
type: "compaction.ended", type: "session.compaction.ended",
...options, ...options,
schema: { schema: {
...Base, ...Base,
@@ -473,13 +473,13 @@ export namespace Compaction {
export namespace RevertEvent { export namespace RevertEvent {
export const Staged = Event.durable({ export const Staged = Event.durable({
type: "revert.staged", type: "session.revert.staged",
...options, ...options,
schema: { ...Base, revert: Revert.State }, schema: { ...Base, revert: Revert.State },
}) })
export const Cleared = Event.durable({ type: "revert.cleared", ...options, schema: Base }) export const Cleared = Event.durable({ type: "session.revert.cleared", ...options, schema: Base })
export const Committed = Event.durable({ export const Committed = Event.durable({
type: "revert.committed", type: "session.revert.committed",
...options, ...options,
schema: { ...Base, messageID: SessionMessage.ID }, schema: { ...Base, messageID: SessionMessage.ID },
}) })
+3 -3
View File
@@ -9,6 +9,7 @@ import { DateTimeUtcFromMillis, RelativePath, statics } from "./schema.js"
import { SessionID } from "./session-id.js" import { SessionID } from "./session-id.js"
import { ascending } from "./identifier.js" import { ascending } from "./identifier.js"
import { Event } from "./event.js" import { Event } from "./event.js"
import { Shell as ShellSchema } from "./shell.js"
export const ID = Schema.String.check(Schema.isStartsWith("msg_")).pipe( export const ID = Schema.String.check(Schema.isStartsWith("msg_")).pipe(
Schema.brand("Session.Message.ID"), Schema.brand("Session.Message.ID"),
@@ -82,9 +83,8 @@ export interface Shell extends Schema.Schema.Type<typeof Shell> {}
export const Shell = Schema.Struct({ export const Shell = Schema.Struct({
...Base, ...Base,
type: Schema.Literal("shell"), type: Schema.Literal("shell"),
callID: Schema.String, shell: ShellSchema.Info,
command: Schema.String, output: ShellSchema.Output.pipe(optional),
output: Schema.String,
time: Schema.Struct({ time: Schema.Struct({
created: DateTimeUtcFromMillis, created: DateTimeUtcFromMillis,
completed: DateTimeUtcFromMillis.pipe(optional), completed: DateTimeUtcFromMillis.pipe(optional),
+32 -32
View File
@@ -53,7 +53,7 @@ describe("public event manifest", () => {
expect(Session.Event.Definitions).toBe(SessionEvent.Definitions) expect(Session.Event.Definitions).toBe(SessionEvent.Definitions)
expect(Workspace.Event).toBe(WorkspaceEvent) expect(Workspace.Event).toBe(WorkspaceEvent)
expect(Workspace.Event.Definitions).toBe(WorkspaceEvent.Definitions) expect(Workspace.Event.Definitions).toBe(WorkspaceEvent.Definitions)
expect(EventManifest.Latest.get("step.ended")).toBe(SessionEvent.Step.Ended) expect(EventManifest.Latest.get("session.step.ended")).toBe(SessionEvent.Step.Ended)
expect(EventManifest.Latest.get("todo.updated")).toBe(SessionTodo.Event.Updated) expect(EventManifest.Latest.get("todo.updated")).toBe(SessionTodo.Event.Updated)
expect(EventManifest.Latest.get("agent.updated")).toBe(Agent.Event.Updated) expect(EventManifest.Latest.get("agent.updated")).toBe(Agent.Event.Updated)
expect(EventManifest.Latest.get("project.updated")).toBe(Project.Event.Updated) expect(EventManifest.Latest.get("project.updated")).toBe(Project.Event.Updated)
@@ -74,8 +74,8 @@ describe("public event manifest", () => {
SessionV1.Event.Diff, SessionV1.Event.Diff,
SessionV1.Event.Error, SessionV1.Event.Error,
]) ])
expect(EventManifest.Durable.get("step.ended.1")).toBe(SessionEvent.Step.Ended) expect(EventManifest.Durable.get("session.step.ended.1")).toBe(SessionEvent.Step.Ended)
expect(EventManifest.Durable.has("step.ended.2")).toBe(false) expect(EventManifest.Durable.has("session.step.ended.2")).toBe(false)
}) })
test("derives durable definitions from explicit definition durability", () => { test("derives durable definitions from explicit definition durability", () => {
@@ -88,37 +88,37 @@ describe("public event manifest", () => {
"message.removed.1", "message.removed.1",
"message.part.updated.1", "message.part.updated.1",
"message.part.removed.1", "message.part.removed.1",
"agent.selected.1", "session.agent.selected.1",
"model.selected.1", "session.model.selected.1",
"session.moved.1", "session.moved.1",
"renamed.1", "session.renamed.1",
"forked.1", "session.forked.1",
"prompt.promoted.1", "session.prompt.promoted.1",
"prompt.admitted.1", "session.prompt.admitted.1",
"session.context.updated.1", "session.context.updated.1",
"synthetic.1", "session.synthetic.1",
"skill.activated.1", "session.skill.activated.1",
"shell.started.1", "session.shell.started.1",
"shell.ended.1", "session.shell.ended.1",
"step.started.1", "session.step.started.1",
"step.ended.1", "session.step.ended.1",
"step.failed.1", "session.step.failed.1",
"text.started.1", "session.text.started.1",
"text.ended.1", "session.text.ended.1",
"tool.input.started.1", "session.tool.input.started.1",
"tool.input.ended.1", "session.tool.input.ended.1",
"tool.called.1", "session.tool.called.1",
"tool.progress.1", "session.tool.progress.1",
"tool.success.1", "session.tool.success.1",
"tool.failed.1", "session.tool.failed.1",
"reasoning.started.1", "session.reasoning.started.1",
"reasoning.ended.1", "session.reasoning.ended.1",
"retried.1", "session.retried.1",
"compaction.started.1", "session.compaction.started.1",
"compaction.ended.1", "session.compaction.ended.1",
"revert.staged.1", "session.revert.staged.1",
"revert.cleared.1", "session.revert.cleared.1",
"revert.committed.1", "session.revert.committed.1",
].toSorted(), ].toSorted(),
) )
expect(SessionEvent.DurableDefinitions).toEqual( expect(SessionEvent.DurableDefinitions).toEqual(
+6 -6
View File
@@ -78,7 +78,7 @@ it.live(
prompt: fixture.sdk.Prompt.make({ text: "Promote this input" }), prompt: fixture.sdk.Prompt.make({ text: "Promote this input" }),
}) })
const prompted = yield* opencode.sessions.log({ sessionID: id, follow: true }).pipe( const prompted = yield* opencode.sessions.log({ sessionID: id, follow: true }).pipe(
Stream.filter((event) => event.type === "prompt.promoted" && event.data.inputID === wake.id), Stream.filter((event) => event.type === "session.prompt.promoted" && event.data.inputID === wake.id),
Stream.runHead, Stream.runHead,
Effect.timeout("10 seconds"), Effect.timeout("10 seconds"),
Effect.map(Option.getOrThrow), Effect.map(Option.getOrThrow),
@@ -119,7 +119,7 @@ it.live(
expect(page.data.some((session) => session.id === id)).toBe(true) expect(page.data.some((session) => session.id === id)).toBe(true)
expect(active).toEqual({ data: {}, watermarks: {} }) expect(active).toEqual({ data: {}, watermarks: {} })
expect(admitted.sessionID).toBe(id) expect(admitted.sessionID).toBe(id)
expect(prompted.type).toBe("prompt.promoted") expect(prompted.type).toBe("session.prompt.promoted")
expect(wakeContext).toContainEqual(expect.objectContaining({ id: wake.id, type: "user" })) expect(wakeContext).toContainEqual(expect.objectContaining({ id: wake.id, type: "user" }))
expect(contextEntries).toEqual([ expect(contextEntries).toEqual([
{ key: "deploy-target", value: "production" }, { key: "deploy-target", value: "production" },
@@ -127,7 +127,7 @@ it.live(
]) ])
expect(remainingContextEntries).toEqual([{ key: "deploy-target", value: "production" }]) expect(remainingContextEntries).toEqual([{ key: "deploy-target", value: "production" }])
expect(context.some((message) => message.type === "model-switched")).toBe(true) expect(context.some((message) => message.type === "model-switched")).toBe(true)
expect(event).toMatchObject({ type: "model.selected", durable: { seq: 1 } }) expect(event).toMatchObject({ type: "session.model.selected", durable: { seq: 1 } })
expect(message).toEqual(modelMessage) expect(message).toEqual(modelMessage)
expect(missing.map((error) => error._tag)).toEqual([ expect(missing.map((error) => error._tag)).toEqual([
"SessionNotFoundError", "SessionNotFoundError",
@@ -149,13 +149,13 @@ it.live(
const opencode = yield* fixture.sdk.OpenCode.create() const opencode = yield* fixture.sdk.OpenCode.create()
const id = sessionID(fixture) const id = sessionID(fixture)
const connected = yield* Latch.make(false) const connected = yield* Latch.make(false)
const prompted = yield* Deferred.make<Extract<OpenCodeEvent, { type: "prompt.promoted" }>>() const prompted = yield* Deferred.make<Extract<OpenCodeEvent, { type: "session.prompt.promoted" }>>()
yield* opencode.events.subscribe().pipe( yield* opencode.events.subscribe().pipe(
Stream.runForEach((event) => Stream.runForEach((event) =>
event.type === "server.connected" event.type === "server.connected"
? connected.open ? connected.open
: event.type === "prompt.promoted" && event.data.sessionID === id : event.type === "session.prompt.promoted" && event.data.sessionID === id
? Deferred.succeed(prompted, event).pipe(Effect.asVoid) ? Deferred.succeed(prompted, event).pipe(Effect.asVoid)
: Effect.void, : Effect.void,
), ),
@@ -191,7 +191,7 @@ it.live(
Stream.runForEach((notification: OpenCodeEvent) => Stream.runForEach((notification: OpenCodeEvent) =>
notification.type === "server.connected" notification.type === "server.connected"
? ready.open ? ready.open
: notification.type === "agent.selected" && notification.data.sessionID === id : notification.type === "session.agent.selected" && notification.data.sessionID === id
? event.open ? event.open
: Effect.void, : Effect.void,
) )
+2 -2
View File
@@ -60,7 +60,7 @@ if (schemas) {
visit({ ...document, components: { ...document.components, schemas: undefined } }) visit({ ...document, components: { ...document.components, schemas: undefined } })
for (const name of Object.keys(schemas)) { for (const name of Object.keys(schemas)) {
if ( if (
/^(AgentSelected|ModelSelected|SessionMoved|Renamed|Forked|PromptPromoted|PromptAdmitted|ExecutionSettled|ContextUpdated|Synthetic|SkillActivated|ShellStarted|ShellEnded|StepStarted|StepEnded|StepFailed|TextStarted|TextDelta|TextEnded|ReasoningStarted|ReasoningDelta|ReasoningEnded|ToolInputStarted|ToolInputDelta|ToolInputEnded|ToolCalled|ToolProgress|ToolSuccess|ToolFailed|Retried|CompactionStarted|CompactionDelta|CompactionEnded|RevertStaged|RevertCleared|RevertCommitted)1$/.test( /^(SessionAgentSelected|SessionModelSelected|SessionMoved|SessionRenamed|SessionForked|SessionPromptPromoted|SessionPromptAdmitted|SessionExecutionSettled|SessionContextUpdated|SessionSynthetic|SessionSkillActivated|SessionShellStarted|SessionShellEnded|SessionStepStarted|SessionStepEnded|SessionStepFailed|SessionTextStarted|SessionTextDelta|SessionTextEnded|SessionReasoningStarted|SessionReasoningDelta|SessionReasoningEnded|SessionToolInputStarted|SessionToolInputDelta|SessionToolInputEnded|SessionToolCalled|SessionToolProgress|SessionToolSuccess|SessionToolFailed|SessionRetried|SessionCompactionStarted|SessionCompactionDelta|SessionCompactionEnded|SessionRevertStaged|SessionRevertCleared|SessionRevertCommitted)1$/.test(
name, name,
) && ) &&
!reachable.has(name) !reachable.has(name)
@@ -100,7 +100,7 @@ await createClient({
const generatedTypesPath = "./src/v2/gen/types.gen.ts" const generatedTypesPath = "./src/v2/gen/types.gen.ts"
const generatedTypes = await Bun.file(generatedTypesPath).text() const generatedTypes = await Bun.file(generatedTypesPath).text()
if ( if (
/export type (AgentSelected|ModelSelected|SessionMoved|Renamed|Forked|PromptPromoted|PromptAdmitted|ExecutionSettled|ContextUpdated|Synthetic|SkillActivated|ShellStarted|ShellEnded|StepStarted|StepEnded|StepFailed|TextStarted|TextDelta|TextEnded|ReasoningStarted|ReasoningDelta|ReasoningEnded|ToolInputStarted|ToolInputDelta|ToolInputEnded|ToolCalled|ToolProgress|ToolSuccess|ToolFailed|Retried|CompactionStarted|CompactionDelta|CompactionEnded|RevertStaged|RevertCleared|RevertCommitted)1 =/.test( /export type (SessionAgentSelected|SessionModelSelected|SessionMoved|SessionRenamed|SessionForked|SessionPromptPromoted|SessionPromptAdmitted|SessionExecutionSettled|SessionContextUpdated|SessionSynthetic|SessionSkillActivated|SessionShellStarted|SessionShellEnded|SessionStepStarted|SessionStepEnded|SessionStepFailed|SessionTextStarted|SessionTextDelta|SessionTextEnded|SessionReasoningStarted|SessionReasoningDelta|SessionReasoningEnded|SessionToolInputStarted|SessionToolInputDelta|SessionToolInputEnded|SessionToolCalled|SessionToolProgress|SessionToolSuccess|SessionToolFailed|SessionRetried|SessionCompactionStarted|SessionCompactionDelta|SessionCompactionEnded|SessionRevertStaged|SessionRevertCleared|SessionRevertCommitted)1 =/.test(
generatedTypes, generatedTypes,
) )
) { ) {
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -16410,7 +16410,7 @@
"text": { "text": {
"type": "string" "type": "string"
}, },
"synthetic": { "session.synthetic": {
"type": "boolean" "type": "boolean"
}, },
"ignored": { "ignored": {
@@ -23071,7 +23071,7 @@
"text": { "text": {
"type": "string" "type": "string"
}, },
"synthetic": { "session.synthetic": {
"type": "boolean" "type": "boolean"
}, },
"ignored": { "ignored": {
@@ -27479,7 +27479,7 @@
}, },
"type": { "type": {
"type": "string", "type": "string",
"enum": ["synthetic"] "enum": ["session.synthetic"]
} }
}, },
"required": ["id", "time", "sessionID", "text", "type"], "required": ["id", "time", "sessionID", "text", "type"],
+38 -39
View File
@@ -126,8 +126,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
const item = position === undefined ? undefined : messages[position] const item = position === undefined ? undefined : messages[position]
return item?.type === "assistant" ? item : undefined return item?.type === "assistant" ? item : undefined
}, },
activeShell(messages: SessionMessage[], callID: string) { shell(messages: SessionMessage[], shellID: string) {
const item = messages.findLast((item) => item.type === "shell" && item.callID === callID) const item = messages.findLast((item) => item.type === "shell" && item.shell.id === shellID)
return item?.type === "shell" ? item : undefined return item?.type === "shell" ? item : undefined
}, },
latestTool(assistant: SessionMessageAssistant | undefined, callID?: string) { latestTool(assistant: SessionMessageAssistant | undefined, callID?: string) {
@@ -220,7 +220,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
case "skill.updated": case "skill.updated":
void result.location.skill.refresh(event.location) void result.location.skill.refresh(event.location)
break break
case "agent.selected": case "session.agent.selected":
if (store.session.info[event.data.sessionID]) if (store.session.info[event.data.sessionID])
setStore("session", "info", event.data.sessionID, "agent", event.data.agent) setStore("session", "info", event.data.sessionID, "agent", event.data.agent)
message.update(event.data.sessionID, (draft, index) => { message.update(event.data.sessionID, (draft, index) => {
@@ -232,7 +232,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
}) })
}) })
break break
case "model.selected": case "session.model.selected":
if (store.session.info[event.data.sessionID]) if (store.session.info[event.data.sessionID])
setStore("session", "info", event.data.sessionID, "model", event.data.model) setStore("session", "info", event.data.sessionID, "model", event.data.model)
message.update(event.data.sessionID, (draft, index) => { message.update(event.data.sessionID, (draft, index) => {
@@ -244,11 +244,11 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
}) })
}) })
break break
case "renamed": case "session.renamed":
if (store.session.info[event.data.sessionID]) if (store.session.info[event.data.sessionID])
setStore("session", "info", event.data.sessionID, "title", event.data.title) setStore("session", "info", event.data.sessionID, "title", event.data.title)
break break
case "prompt.promoted": { case "session.prompt.promoted": {
setStore("session", "status", event.data.sessionID, "running") setStore("session", "status", event.data.sessionID, "running")
message.update(event.data.sessionID, (draft, index) => { message.update(event.data.sessionID, (draft, index) => {
const position = index.get(event.data.inputID) const position = index.get(event.data.inputID)
@@ -264,7 +264,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
}) })
break break
} }
case "prompt.admitted": case "session.prompt.admitted":
message.update(event.data.sessionID, (draft, index) => { message.update(event.data.sessionID, (draft, index) => {
message.append(draft, index, { message.append(draft, index, {
id: event.data.inputID, id: event.data.inputID,
@@ -287,7 +287,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
}) })
}) })
break break
case "synthetic": case "session.synthetic":
message.update(event.data.sessionID, (draft, index) => { message.update(event.data.sessionID, (draft, index) => {
message.append(draft, index, { message.append(draft, index, {
id: messageIDFromEvent(event.id), id: messageIDFromEvent(event.id),
@@ -299,29 +299,28 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
}) })
}) })
break break
case "shell.started": case "session.shell.started":
setStore("session", "status", event.data.sessionID, "running") setStore("session", "status", event.data.sessionID, "running")
message.update(event.data.sessionID, (draft, index) => { message.update(event.data.sessionID, (draft, index) => {
message.append(draft, index, { message.append(draft, index, {
id: messageIDFromEvent(event.id), id: messageIDFromEvent(event.id),
type: "shell", type: "shell",
callID: event.data.callID, shell: event.data.shell,
command: event.data.command,
output: "",
time: { created: event.created }, time: { created: event.created },
}) })
}) })
break break
case "shell.ended": case "session.shell.ended":
setStore("session", "status", event.data.sessionID, "idle") setStore("session", "status", event.data.sessionID, "idle")
message.update(event.data.sessionID, (draft, index) => { message.update(event.data.sessionID, (draft) => {
const match = message.activeShell(draft, event.data.callID) const match = message.shell(draft, event.data.shell.id)
if (!match) return if (!match) return
match.shell = event.data.shell
match.output = event.data.output match.output = event.data.output
match.time.completed = event.created match.time.completed = event.created
}) })
break break
case "step.started": case "session.step.started":
setStore("session", "status", event.data.sessionID, "running") setStore("session", "status", event.data.sessionID, "running")
message.update(event.data.sessionID, (draft, index) => { message.update(event.data.sessionID, (draft, index) => {
if (index.has(event.data.assistantMessageID)) return if (index.has(event.data.assistantMessageID)) return
@@ -338,7 +337,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
}) })
}) })
break break
case "step.ended": case "session.step.ended":
setStore("session", "status", event.data.sessionID, "running") setStore("session", "status", event.data.sessionID, "running")
message.update(event.data.sessionID, (draft, index) => { message.update(event.data.sessionID, (draft, index) => {
const currentAssistant = message.assistant(draft, index, event.data.assistantMessageID) const currentAssistant = message.assistant(draft, index, event.data.assistantMessageID)
@@ -351,7 +350,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
currentAssistant.snapshot = { ...currentAssistant.snapshot, end: event.data.snapshot } currentAssistant.snapshot = { ...currentAssistant.snapshot, end: event.data.snapshot }
}) })
break break
case "step.failed": case "session.step.failed":
message.update(event.data.sessionID, (draft, index) => { message.update(event.data.sessionID, (draft, index) => {
const currentAssistant = message.assistant(draft, index, event.data.assistantMessageID) const currentAssistant = message.assistant(draft, index, event.data.assistantMessageID)
if (!currentAssistant) return if (!currentAssistant) return
@@ -360,7 +359,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
currentAssistant.error = event.data.error currentAssistant.error = event.data.error
}) })
break break
case "text.started": case "session.text.started":
message.update(event.data.sessionID, (draft, index) => { message.update(event.data.sessionID, (draft, index) => {
message.assistant(draft, index, event.data.assistantMessageID)?.content.push({ message.assistant(draft, index, event.data.assistantMessageID)?.content.push({
type: "text", type: "text",
@@ -369,7 +368,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
}) })
}) })
break break
case "text.delta": case "session.text.delta":
message.update(event.data.sessionID, (draft, index) => { message.update(event.data.sessionID, (draft, index) => {
const match = message.latestText( const match = message.latestText(
message.assistant(draft, index, event.data.assistantMessageID), message.assistant(draft, index, event.data.assistantMessageID),
@@ -378,7 +377,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
if (match) match.text += event.data.delta if (match) match.text += event.data.delta
}) })
break break
case "text.ended": case "session.text.ended":
message.update(event.data.sessionID, (draft, index) => { message.update(event.data.sessionID, (draft, index) => {
const match = message.latestText( const match = message.latestText(
message.assistant(draft, index, event.data.assistantMessageID), message.assistant(draft, index, event.data.assistantMessageID),
@@ -387,7 +386,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
if (match) match.text = event.data.text if (match) match.text = event.data.text
}) })
break break
case "tool.input.started": case "session.tool.input.started":
message.update(event.data.sessionID, (draft, index) => { message.update(event.data.sessionID, (draft, index) => {
message.assistant(draft, index, event.data.assistantMessageID)?.content.push({ message.assistant(draft, index, event.data.assistantMessageID)?.content.push({
type: "tool", type: "tool",
@@ -398,7 +397,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
}) })
}) })
break break
case "tool.input.delta": case "session.tool.input.delta":
message.update(event.data.sessionID, (draft, index) => { message.update(event.data.sessionID, (draft, index) => {
const match = message.latestTool( const match = message.latestTool(
message.assistant(draft, index, event.data.assistantMessageID), message.assistant(draft, index, event.data.assistantMessageID),
@@ -407,7 +406,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
if (match?.state.status === "pending") match.state.input += event.data.delta if (match?.state.status === "pending") match.state.input += event.data.delta
}) })
break break
case "tool.input.ended": case "session.tool.input.ended":
message.update(event.data.sessionID, (draft, index) => { message.update(event.data.sessionID, (draft, index) => {
const match = message.latestTool( const match = message.latestTool(
message.assistant(draft, index, event.data.assistantMessageID), message.assistant(draft, index, event.data.assistantMessageID),
@@ -416,7 +415,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
if (match?.state.status === "pending") match.state.input = event.data.text if (match?.state.status === "pending") match.state.input = event.data.text
}) })
break break
case "tool.called": case "session.tool.called":
message.update(event.data.sessionID, (draft, index) => { message.update(event.data.sessionID, (draft, index) => {
const match = message.latestTool( const match = message.latestTool(
message.assistant(draft, index, event.data.assistantMessageID), message.assistant(draft, index, event.data.assistantMessageID),
@@ -428,7 +427,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
match.state = { status: "running", input: event.data.input, structured: {}, content: [] } match.state = { status: "running", input: event.data.input, structured: {}, content: [] }
}) })
break break
case "tool.progress": case "session.tool.progress":
message.update(event.data.sessionID, (draft, index) => { message.update(event.data.sessionID, (draft, index) => {
const match = message.latestTool( const match = message.latestTool(
message.assistant(draft, index, event.data.assistantMessageID), message.assistant(draft, index, event.data.assistantMessageID),
@@ -439,7 +438,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
match.state.content = [...event.data.content] match.state.content = [...event.data.content]
}) })
break break
case "tool.success": case "session.tool.success":
message.update(event.data.sessionID, (draft, index) => { message.update(event.data.sessionID, (draft, index) => {
const match = message.latestTool( const match = message.latestTool(
message.assistant(draft, index, event.data.assistantMessageID), message.assistant(draft, index, event.data.assistantMessageID),
@@ -461,7 +460,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
match.time.completed = event.created match.time.completed = event.created
}) })
break break
case "tool.failed": case "session.tool.failed":
message.update(event.data.sessionID, (draft, index) => { message.update(event.data.sessionID, (draft, index) => {
const match = message.latestTool( const match = message.latestTool(
message.assistant(draft, index, event.data.assistantMessageID), message.assistant(draft, index, event.data.assistantMessageID),
@@ -484,7 +483,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
match.time.completed = event.created match.time.completed = event.created
}) })
break break
case "reasoning.started": case "session.reasoning.started":
message.update(event.data.sessionID, (draft, index) => { message.update(event.data.sessionID, (draft, index) => {
message.assistant(draft, index, event.data.assistantMessageID)?.content.push({ message.assistant(draft, index, event.data.assistantMessageID)?.content.push({
type: "reasoning", type: "reasoning",
@@ -495,7 +494,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
}) })
}) })
break break
case "reasoning.delta": case "session.reasoning.delta":
message.update(event.data.sessionID, (draft, index) => { message.update(event.data.sessionID, (draft, index) => {
const match = message.latestReasoning( const match = message.latestReasoning(
message.assistant(draft, index, event.data.assistantMessageID), message.assistant(draft, index, event.data.assistantMessageID),
@@ -504,7 +503,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
if (match) match.text += event.data.delta if (match) match.text += event.data.delta
}) })
break break
case "reasoning.ended": case "session.reasoning.ended":
message.update(event.data.sessionID, (draft, index) => { message.update(event.data.sessionID, (draft, index) => {
const match = message.latestReasoning( const match = message.latestReasoning(
message.assistant(draft, index, event.data.assistantMessageID), message.assistant(draft, index, event.data.assistantMessageID),
@@ -517,25 +516,25 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
} }
}) })
break break
case "retried": case "session.retried":
case "compaction.started": case "session.compaction.started":
setStore("session", "status", event.data.sessionID, "running") setStore("session", "status", event.data.sessionID, "running")
break break
case "execution.settled": case "session.execution.settled":
setStore("session", "status", event.data.sessionID, "idle") setStore("session", "status", event.data.sessionID, "idle")
break break
case "revert.staged": case "session.revert.staged":
if (store.session.info[event.data.sessionID]) if (store.session.info[event.data.sessionID])
setStore("session", "info", event.data.sessionID, "revert", event.data.revert) setStore("session", "info", event.data.sessionID, "revert", event.data.revert)
break break
case "revert.cleared": case "session.revert.cleared":
case "revert.committed": case "session.revert.committed":
if (store.session.info[event.data.sessionID]) if (store.session.info[event.data.sessionID])
setStore("session", "info", event.data.sessionID, "revert", undefined) setStore("session", "info", event.data.sessionID, "revert", undefined)
break break
case "compaction.delta": case "session.compaction.delta":
break break
case "compaction.ended": case "session.compaction.ended":
message.update(event.data.sessionID, (draft, index) => { message.update(event.data.sessionID, (draft, index) => {
message.append(draft, index, { message.append(draft, index, {
id: messageIDFromEvent(event.id), id: messageIDFromEvent(event.id),
@@ -74,17 +74,17 @@ const tui: TuiPlugin = async (api) => {
notify(api, sessionID, "Session done", session?.parentID ? "subagent_done" : "done") notify(api, sessionID, "Session done", session?.parentID ? "subagent_done" : "done")
} }
api.event.on("prompt.promoted", (event) => started(event.data.sessionID)) api.event.on("session.prompt.promoted", (event) => started(event.data.sessionID))
api.event.on("shell.started", (event) => started(event.data.sessionID)) api.event.on("session.shell.started", (event) => started(event.data.sessionID))
api.event.on("step.started", (event) => started(event.data.sessionID)) api.event.on("session.step.started", (event) => started(event.data.sessionID))
api.event.on("retried", (event) => started(event.data.sessionID)) api.event.on("session.retried", (event) => started(event.data.sessionID))
api.event.on("compaction.started", (event) => started(event.data.sessionID)) api.event.on("session.compaction.started", (event) => started(event.data.sessionID))
api.event.on("shell.ended", (event) => ended(event.data.sessionID)) api.event.on("session.shell.ended", (event) => ended(event.data.sessionID))
api.event.on("step.ended", (event) => { api.event.on("session.step.ended", (event) => {
if (event.data.finish === "tool-calls") return if (event.data.finish === "tool-calls") return
ended(event.data.sessionID) ended(event.data.sessionID)
}) })
api.event.on("step.failed", (event) => { api.event.on("session.step.failed", (event) => {
const sessionID = event.data.sessionID const sessionID = event.data.sessionID
if (!active.has(sessionID)) return if (!active.has(sessionID)) return
errored.add(sessionID) errored.add(sessionID)
+7 -11
View File
@@ -1346,7 +1346,7 @@ function RevertMessage(props: {
function ShellMessage(props: { message: Extract<SessionMessage, { type: "shell" }> }) { function ShellMessage(props: { message: Extract<SessionMessage, { type: "shell" }> }) {
const { theme } = useTheme() const { theme } = useTheme()
const output = createMemo(() => stripAnsi(props.message.output.trim())) const output = createMemo(() => stripAnsi(props.message.output?.output.trim() ?? ""))
return ( return (
<box <box
@@ -1359,7 +1359,7 @@ function ShellMessage(props: { message: Extract<SessionMessage, { type: "shell"
customBorderChars={SplitBorder.customBorderChars} customBorderChars={SplitBorder.customBorderChars}
borderColor={theme.background} borderColor={theme.background}
> >
<text fg={theme.text}>$ {props.message.command}</text> <text fg={theme.text}>$ {props.message.shell.command}</text>
<Show when={output()}> <Show when={output()}>
<text fg={theme.textMuted}>{output()}</text> <text fg={theme.textMuted}>{output()}</text>
</Show> </Show>
@@ -1408,13 +1408,7 @@ function UserMessage(props: { message: SessionMessageUser }) {
> >
<text fg={theme.text}>{props.message.text}</text> <text fg={theme.text}>{props.message.text}</text>
<Show when={files().length}> <Show when={files().length}>
<box <box flexDirection="row" paddingBottom={metadataVisible() ? 1 : 0} paddingTop={1} gap={1} flexWrap="wrap">
flexDirection="row"
paddingBottom={metadataVisible() ? 1 : 0}
paddingTop={1}
gap={1}
flexWrap="wrap"
>
<For each={files()}> <For each={files()}>
{(file) => { {(file) => {
const directory = file.mime === "application/x-directory" const directory = file.mime === "application/x-directory"
@@ -1744,7 +1738,8 @@ function ToolPart(props: { part: SessionMessageAssistantTool }) {
return Boolean(shellID && data.shell.get(shellID)) return Boolean(shellID && data.shell.get(shellID))
} }
if (display() === "subagent") { if (display() === "subagent") {
const sessionID = stringValue(props.part.state.structured.sessionID) ?? stringValue(props.part.state.structured.sessionId) const sessionID =
stringValue(props.part.state.structured.sessionID) ?? stringValue(props.part.state.structured.sessionId)
return Boolean(sessionID && data.session.status(sessionID) === "running") return Boolean(sessionID && data.session.status(sessionID) === "running")
} }
return false return false
@@ -2660,7 +2655,8 @@ function formatSessionTranscript(
) { ) {
const body = messages.flatMap((message) => { const body = messages.flatMap((message) => {
if (message.type === "user") return [`## User\n\n${message.text}`] if (message.type === "user") return [`## User\n\n${message.text}`]
if (message.type === "shell") return [`## Shell\n\n\`\`\`\n$ ${message.command}\n${message.output}\n\`\`\``] if (message.type === "shell")
return [`## Shell\n\n\`\`\`\n$ ${message.shell.command}\n${message.output?.output ?? ""}\n\`\`\``]
if (message.type !== "assistant") return [] if (message.type !== "assistant") return []
const content = message.content.flatMap((item) => { const content = message.content.flatMap((item) => {
if (item.type === "text") return [item.text] if (item.type === "text") return [item.text]
+14 -14
View File
@@ -133,42 +133,42 @@ export function createSessionRows(sessionID: Accessor<string>) {
if (event.data.sessionID === sessionID()) appendMessage(event.data.inputID) if (event.data.sessionID === sessionID()) appendMessage(event.data.inputID)
} }
const subscriptions = [ const subscriptions = [
data.on("prompt.admitted", input), data.on("session.prompt.admitted", input),
data.on("prompt.promoted", input), data.on("session.prompt.promoted", input),
data.on("session.context.updated", message), data.on("session.context.updated", message),
data.on("synthetic", (event) => { data.on("session.synthetic", (event) => {
if (event.data.sessionID === sessionID() && event.data.description?.trim()) if (event.data.sessionID === sessionID() && event.data.description?.trim())
appendMessage(event.id.replace(/^evt_/, "msg_")) appendMessage(event.id.replace(/^evt_/, "msg_"))
}), }),
data.on("shell.started", message), data.on("session.shell.started", message),
data.on("agent.selected", message), data.on("session.agent.selected", message),
data.on("model.selected", message), data.on("session.model.selected", message),
data.on("compaction.ended", message), data.on("session.compaction.ended", message),
data.on("text.delta", (event) => { data.on("session.text.delta", (event) => {
if (event.data.sessionID === sessionID()) if (event.data.sessionID === sessionID())
appendPart({ messageID: event.data.assistantMessageID, partID: event.data.textID }) appendPart({ messageID: event.data.assistantMessageID, partID: event.data.textID })
}), }),
data.on("text.ended", (event) => { data.on("session.text.ended", (event) => {
if (event.data.sessionID === sessionID() && event.data.text.trim()) if (event.data.sessionID === sessionID() && event.data.text.trim())
appendPart({ messageID: event.data.assistantMessageID, partID: event.data.textID }) appendPart({ messageID: event.data.assistantMessageID, partID: event.data.textID })
}), }),
data.on("reasoning.delta", (event) => { data.on("session.reasoning.delta", (event) => {
if (event.data.sessionID === sessionID()) if (event.data.sessionID === sessionID())
appendPart({ messageID: event.data.assistantMessageID, partID: event.data.reasoningID }) appendPart({ messageID: event.data.assistantMessageID, partID: event.data.reasoningID })
}), }),
data.on("reasoning.ended", (event) => { data.on("session.reasoning.ended", (event) => {
if (event.data.sessionID === sessionID() && event.data.text.trim()) if (event.data.sessionID === sessionID() && event.data.text.trim())
appendPart({ messageID: event.data.assistantMessageID, partID: event.data.reasoningID }) appendPart({ messageID: event.data.assistantMessageID, partID: event.data.reasoningID })
}), }),
data.on("tool.input.started", (event) => { data.on("session.tool.input.started", (event) => {
if (event.data.sessionID === sessionID()) if (event.data.sessionID === sessionID())
appendPart({ messageID: event.data.assistantMessageID, partID: event.data.callID }, event.data.name) appendPart({ messageID: event.data.assistantMessageID, partID: event.data.callID }, event.data.name)
}), }),
data.on("step.ended", (event) => { data.on("session.step.ended", (event) => {
if (event.data.sessionID !== sessionID() || ["tool-calls", "unknown"].includes(event.data.finish)) return if (event.data.sessionID !== sessionID() || ["tool-calls", "unknown"].includes(event.data.finish)) return
appendFooter(event.data.assistantMessageID) appendFooter(event.data.assistantMessageID)
}), }),
data.on("step.failed", (event) => { data.on("session.step.failed", (event) => {
if (event.data.sessionID === sessionID()) appendFooter(event.data.assistantMessageID) if (event.data.sessionID === sessionID()) appendFooter(event.data.assistantMessageID)
}), }),
] ]
@@ -91,7 +91,7 @@ function stepStarted(id: string, sessionID = "session"): V2Event {
return { return {
id, id,
created: 0, created: 0,
type: "step.started", type: "session.step.started",
durable: durable(sessionID), durable: durable(sessionID),
data: { data: {
sessionID, sessionID,
@@ -106,7 +106,7 @@ function stepEnded(id: string, sessionID = "session", finish = "stop"): V2Event
return { return {
id, id,
created: 0, created: 0,
type: "step.ended", type: "session.step.ended",
durable: durable(sessionID), durable: durable(sessionID),
data: { data: {
sessionID, sessionID,
@@ -122,7 +122,7 @@ function stepFailed(id: string, sessionID = "session"): V2Event {
return { return {
id, id,
created: 0, created: 0,
type: "step.failed", type: "session.step.failed",
durable: durable(sessionID), durable: durable(sessionID),
data: { data: {
sessionID, sessionID,
+16 -16
View File
@@ -269,7 +269,7 @@ test("tracks session status from active sessions and execution events", async ()
emitEvent(events, { emitEvent(events, {
id: "evt_step_started", id: "evt_step_started",
created: 0, created: 0,
type: "step.started", type: "session.step.started",
durable: durable("session-live"), durable: durable("session-live"),
data: { data: {
sessionID: "session-live", sessionID: "session-live",
@@ -283,7 +283,7 @@ test("tracks session status from active sessions and execution events", async ()
emitEvent(events, { emitEvent(events, {
id: "evt_step_ended", id: "evt_step_ended",
created: 0, created: 0,
type: "step.ended", type: "session.step.ended",
durable: durable("session-live", 1, 2), durable: durable("session-live", 1, 2),
data: { data: {
sessionID: "session-live", sessionID: "session-live",
@@ -302,7 +302,7 @@ test("tracks session status from active sessions and execution events", async ()
emitEvent(events, { emitEvent(events, {
id: "evt_execution_settled", id: "evt_execution_settled",
created: 0, created: 0,
type: "execution.settled", type: "session.execution.settled",
data: { data: {
sessionID: "session-live", sessionID: "session-live",
outcome: "success", outcome: "success",
@@ -313,7 +313,7 @@ test("tracks session status from active sessions and execution events", async ()
emitEvent(events, { emitEvent(events, {
id: "evt_failed_step_started", id: "evt_failed_step_started",
created: 0, created: 0,
type: "step.started", type: "session.step.started",
durable: durable("session-failed"), durable: durable("session-failed"),
data: { data: {
sessionID: "session-failed", sessionID: "session-failed",
@@ -327,7 +327,7 @@ test("tracks session status from active sessions and execution events", async ()
emitEvent(events, { emitEvent(events, {
id: "evt_step_failed", id: "evt_step_failed",
created: 0, created: 0,
type: "step.failed", type: "session.step.failed",
durable: durable("session-failed", 1, 2), durable: durable("session-failed", 1, 2),
data: { data: {
sessionID: "session-failed", sessionID: "session-failed",
@@ -344,7 +344,7 @@ test("tracks session status from active sessions and execution events", async ()
emitEvent(events, { emitEvent(events, {
id: "evt_failed_execution_settled", id: "evt_failed_execution_settled",
created: 0, created: 0,
type: "execution.settled", type: "session.execution.settled",
data: { data: {
sessionID: "session-failed", sessionID: "session-failed",
outcome: "failure", outcome: "failure",
@@ -797,14 +797,14 @@ test("settles pending tools when a live failure arrives", async () => {
emitEvent(events, { emitEvent(events, {
id: "evt_agent_1", id: "evt_agent_1",
created: 0, created: 0,
type: "agent.selected", type: "session.agent.selected",
durable: durable("session-1"), durable: durable("session-1"),
data: { sessionID: "session-1", agent: "build" }, data: { sessionID: "session-1", agent: "build" },
}) })
emitEvent(events, { emitEvent(events, {
id: "evt_model_1", id: "evt_model_1",
created: 0, created: 0,
type: "model.selected", type: "session.model.selected",
durable: durable("session-1", 1), durable: durable("session-1", 1),
data: { data: {
sessionID: "session-1", sessionID: "session-1",
@@ -814,7 +814,7 @@ test("settles pending tools when a live failure arrives", async () => {
emitEvent(events, { emitEvent(events, {
id: "evt_step_started_1", id: "evt_step_started_1",
created: 0, created: 0,
type: "step.started", type: "session.step.started",
durable: durable("session-1", 2), durable: durable("session-1", 2),
data: { data: {
sessionID: "session-1", sessionID: "session-1",
@@ -826,7 +826,7 @@ test("settles pending tools when a live failure arrives", async () => {
emitEvent(events, { emitEvent(events, {
id: "evt_input_1", id: "evt_input_1",
created: 0, created: 0,
type: "tool.input.started", type: "session.tool.input.started",
durable: durable("session-1", 3), durable: durable("session-1", 3),
data: { data: {
sessionID: "session-1", sessionID: "session-1",
@@ -838,7 +838,7 @@ test("settles pending tools when a live failure arrives", async () => {
emitEvent(events, { emitEvent(events, {
id: "evt_called_1", id: "evt_called_1",
created: 0, created: 0,
type: "tool.called", type: "session.tool.called",
durable: durable("session-1", 4), durable: durable("session-1", 4),
data: { data: {
sessionID: "session-1", sessionID: "session-1",
@@ -852,7 +852,7 @@ test("settles pending tools when a live failure arrives", async () => {
emitEvent(events, { emitEvent(events, {
id: "evt_failed_1", id: "evt_failed_1",
created: 0, created: 0,
type: "tool.failed", type: "session.tool.failed",
durable: durable("session-1", 5), durable: durable("session-1", 5),
data: { data: {
sessionID: "session-1", sessionID: "session-1",
@@ -942,7 +942,7 @@ test("renders admitted prompts immediately with queued marker and clears when pr
emitEvent(events, { emitEvent(events, {
id: "evt_admitted_1", id: "evt_admitted_1",
created: 0, created: 0,
type: "prompt.admitted", type: "session.prompt.admitted",
durable: durable(sessionID), durable: durable(sessionID),
data: { data: {
sessionID, sessionID,
@@ -961,7 +961,7 @@ test("renders admitted prompts immediately with queued marker and clears when pr
emitEvent(events, { emitEvent(events, {
id: "evt_prompted_1", id: "evt_prompted_1",
created: 0, created: 0,
type: "prompt.promoted", type: "session.prompt.promoted",
durable: durable(sessionID, 1), durable: durable(sessionID, 1),
data: { data: {
sessionID, sessionID,
@@ -969,8 +969,8 @@ test("renders admitted prompts immediately with queued marker and clears when pr
}, },
}) })
await wait(() => received.at(-1) === "prompt.promoted") await wait(() => received.at(-1) === "session.prompt.promoted")
expect(received.slice(-2)).toEqual(["prompt.admitted", "prompt.promoted"]) expect(received.slice(-2)).toEqual(["session.prompt.admitted", "session.prompt.promoted"])
unsubscribe() unsubscribe()
const message = sync.session.message.list(sessionID)?.[0] const message = sync.session.message.list(sessionID)?.[0]
expect(message?.type).toBe("user") expect(message?.type).toBe("user")
+1 -1
View File
@@ -9,7 +9,7 @@ export type ProviderIconProps = JSX.SVGElementTags["svg"] & {
export const ProviderIcon: Component<ProviderIconProps> = (props) => { export const ProviderIcon: Component<ProviderIconProps> = (props) => {
const [local, rest] = splitProps(props, ["id", "class", "classList"]) const [local, rest] = splitProps(props, ["id", "class", "classList"])
const resolved = createMemo(() => (iconNames.includes(local.id as IconName) ? local.id : "synthetic")) const resolved = createMemo(() => (iconNames.includes(local.id as IconName) ? local.id : "session.synthetic"))
return ( return (
<svg <svg
data-component="provider-icon" data-component="provider-icon"
+22 -1
View File
@@ -908,7 +908,7 @@ Compatibility:
## 2026-07-03: Normalize Session Event Names And Envelope Time ## 2026-07-03: Normalize Session Event Names And Envelope Time
- Drop the experimental `session.next.` prefix from current Session event names. - Drop the experimental `session.next.` prefix from current Session event names.
- Rename `agent.switched` to `agent.selected`, `model.switched` to `model.selected`, and `prompted` to `prompt.promoted`; add `prompt.admitted` as the durable prompt admission record. - Rename `agent.switched` to `session.agent.selected`, `model.switched` to `session.model.selected`, and `prompted` to `session.prompt.promoted`; add `session.prompt.admitted` as the durable prompt admission record.
- Add an envelope-level `created` timestamp stored on each event row; payload-level `timestamp` fields are removed. - Add an envelope-level `created` timestamp stored on each event row; payload-level `timestamp` fields are removed.
- Remove projection-only `messageID` fields from selected/message-producing events; projected message IDs derive from the event ID. `revert.committed.messageID` remains, and `forked.messageID` is now `forked.from`. - Remove projection-only `messageID` fields from selected/message-producing events; projected message IDs derive from the event ID. `revert.committed.messageID` remains, and `forked.messageID` is now `forked.from`.
- Rename `session.moved.subdirectory` to `subpath` and normalize event-related schema identifiers. - Rename `session.moved.subdirectory` to `subpath` and normalize event-related schema identifiers.
@@ -918,3 +918,24 @@ Compatibility:
- V2 durable events and projections are experimental and are reset by `20260703090000_reset_v2_event_rename_sweep`; existing V2 event rows, event sequences, projected session messages, and admitted inputs are wiped. - V2 durable events and projections are experimental and are reset by `20260703090000_reset_v2_event_rename_sweep`; existing V2 event rows, event sequences, projected session messages, and admitted inputs are wiped.
- All renamed durable event types restart at version 1 under their normalized names. - All renamed durable event types restart at version 1 under their normalized names.
- Generated Promise, Effect, and legacy JavaScript SDK surfaces were regenerated from the normalized schemas. - Generated Promise, Effect, and legacy JavaScript SDK surfaces were regenerated from the normalized schemas.
## 2026-07-03: Restore Session Event Prefix
- Restore the `session.` prefix on current Session event names; the prior rename intended to drop only the `.next.` segment from `session.next.*` names.
- Payloads are unchanged. For example, `step.ended` is now `session.step.ended`, while already-correct names such as `session.moved` and `session.context.updated` are unchanged.
Compatibility:
- Covered by the existing `20260703190000_reset_v2_shell_event_payloads` V2 reset; no additional migration or column change is required.
## 2026-07-03: Align Session Shell Payloads With Shell Service
- Change durable `session.shell.started` to carry `shell: Shell.Info`.
- Change durable `session.shell.ended` to carry the final `shell: Shell.Info` snapshot and structured `output: Shell.Output`.
- Project `SessionMessage.Shell` with the same nested Shell service objects so consumers can render command, exit status, truncation, and paged output consistently.
- Remove the transitional `callID` correlation field from shell events and projected shell messages; shell records now key by `shell.id`.
- Publish `session.shell.started` after the shell process is created so `Shell.Info` reflects the real process. A failed spawn publishes no shell events; the failure surfaces as an error on the shell request itself, so the durable ledger never records a `session.shell.started` without a process that existed.
Compatibility:
- V2 durable events and projections are experimental and are reset by `20260703190000_reset_v2_shell_event_payloads`; existing V2 event rows, event sequences, projected session messages, and admitted inputs are wiped.
+1 -1
View File
@@ -114,7 +114,7 @@ Before each step, the runner estimates the complete model-visible request and co
Compaction keeps the full transcript durable while replacing its active model representation with one hidden checkpoint containing a structured rolling summary and token-bounded serialized recent context. Provider-native assistant, reasoning, and tool messages never survive across the boundary, avoiding signature and encrypted-reasoning failures when the earlier prefix changes. Compaction keeps the full transcript durable while replacing its active model representation with one hidden checkpoint containing a structured rolling summary and token-bounded serialized recent context. Provider-native assistant, reasoning, and tool messages never survive across the boundary, avoiding signature and encrypted-reasoning failures when the earlier prefix changes.
`compaction.started.1` durably identifies the attempt. Compaction deltas are live-only progress. `compaction.ended.1` durably stores the final summary and serialized recent context; only this completed event projects a model-visible compaction message. On the next physical attempt, the runner observes that completed compaction and directly renders a fresh Context Epoch baseline. A failed or interrupted attempt therefore leaves the previous history boundary active. `session.compaction.started.1` durably identifies the attempt. Compaction deltas are live-only progress. `session.compaction.ended.1` durably stores the final summary and serialized recent context; only this completed event projects a model-visible compaction message. On the next physical attempt, the runner observes that completed compaction and directly renders a fresh Context Epoch baseline. A failed or interrupted attempt therefore leaves the previous history boundary active.
Repeated compactions update the previous structured summary with newly compacted messages. The runner then reloads projected history and executes the original pending step. Repeated compactions update the previous structured summary with newly compacted messages. The runner then reloads projected history and executes the original pending step.