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
commit 650d774372
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 1521 additions and 1200 deletions

View file

@ -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="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>

View file

@ -226,7 +226,7 @@ const SettingsProvidersContent: Component = () => {
>
<div class="flex flex-col min-w-0">
<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>
<Tag>{language.t("settings.providers.tag.custom")}</Tag>
</div>

View file

@ -223,7 +223,7 @@ export const SettingsProvidersV2: Component = () => {
<div class="settings-v2-provider-row" data-component="custom-provider-section">
<div class="settings-v2-provider-lead">
<ProviderIcon
id="synthetic"
id="session.synthetic"
width={PROVIDER_ICON_SIZE}
height={PROVIDER_ICON_SIZE}
class="settings-v2-provider-icon shrink-0"

View file

@ -976,9 +976,27 @@ export type SessionContextOutput = {
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number; readonly completed?: number }
readonly type: "shell"
readonly callID: string
readonly command: string
readonly output: 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 | "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
@ -1109,7 +1127,7 @@ export type SessionLogOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly agent: string }
@ -1118,7 +1136,7 @@ export type SessionLogOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -1143,7 +1161,7 @@ export type SessionLogOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly title: string }
@ -1152,7 +1170,7 @@ export type SessionLogOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly parentID: string; readonly from?: string }
@ -1161,7 +1179,7 @@ export type SessionLogOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly inputID: string }
@ -1170,7 +1188,7 @@ export type SessionLogOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -1206,7 +1224,7 @@ export type SessionLogOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -1220,7 +1238,7 @@ export type SessionLogOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly name: string; readonly text: string }
@ -1229,25 +1247,59 @@ export type SessionLogOutput =
readonly id: string
readonly created: number
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 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 created: number
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 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 created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -1262,7 +1314,7 @@ export type SessionLogOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -1284,7 +1336,7 @@ export type SessionLogOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -1297,7 +1349,7 @@ export type SessionLogOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly assistantMessageID: string; readonly textID: string }
@ -1306,7 +1358,7 @@ export type SessionLogOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -1320,7 +1372,7 @@ export type SessionLogOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -1334,7 +1386,7 @@ export type SessionLogOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -1349,7 +1401,7 @@ export type SessionLogOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -1363,7 +1415,7 @@ export type SessionLogOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -1377,7 +1429,7 @@ export type SessionLogOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -1396,7 +1448,7 @@ export type SessionLogOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -1414,7 +1466,7 @@ export type SessionLogOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -1438,7 +1490,7 @@ export type SessionLogOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -1457,7 +1509,7 @@ export type SessionLogOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -1477,7 +1529,7 @@ export type SessionLogOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly reason: "auto" | "manual" }
@ -1486,7 +1538,7 @@ export type SessionLogOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -1500,7 +1552,7 @@ export type SessionLogOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -1524,7 +1576,7 @@ export type SessionLogOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string }
@ -1533,7 +1585,7 @@ export type SessionLogOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly messageID: string }
@ -1617,9 +1669,27 @@ export type SessionMessageOutput = {
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number; readonly completed?: number }
readonly type: "shell"
readonly callID: string
readonly command: string
readonly output: 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 | "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
@ -1798,9 +1868,27 @@ export type MessageListOutput = {
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number; readonly completed?: number }
readonly type: "shell"
readonly callID: string
readonly command: string
readonly output: 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 | "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
@ -4309,7 +4397,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly agent: string }
@ -4318,7 +4406,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -4343,7 +4431,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly title: string }
@ -4352,7 +4440,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly parentID: string; readonly from?: string }
@ -4361,7 +4449,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly inputID: string }
@ -4370,7 +4458,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -4397,7 +4485,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "execution.settled"
readonly type: "session.execution.settled"
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@ -4418,7 +4506,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -4432,7 +4520,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly name: string; readonly text: string }
@ -4441,25 +4529,59 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
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 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 created: number
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 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 created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -4474,7 +4596,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -4496,7 +4618,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -4509,7 +4631,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly assistantMessageID: string; readonly textID: string }
@ -4518,7 +4640,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "text.delta"
readonly type: "session.text.delta"
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@ -4531,7 +4653,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -4545,7 +4667,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -4559,7 +4681,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "reasoning.delta"
readonly type: "session.reasoning.delta"
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@ -4572,7 +4694,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -4587,7 +4709,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -4601,7 +4723,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
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 data: {
readonly sessionID: string
@ -4614,7 +4736,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -4628,7 +4750,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -4647,7 +4769,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -4665,7 +4787,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -4689,7 +4811,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -4708,7 +4830,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -4728,7 +4850,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly reason: "auto" | "manual" }
@ -4737,7 +4859,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "compaction.delta"
readonly type: "session.compaction.delta"
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly text: string }
}
@ -4745,7 +4867,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -4759,7 +4881,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
@ -4783,7 +4905,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string }
@ -4792,7 +4914,7 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
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 location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly messageID: string }

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)
}).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]
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(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(logQueries[0]).toEqual({ after: "0" })
const logged = Array.from(result.log)
expect(logged.map((item) => item.type)).toEqual(["model.selected", "log.synced"])
expect(logged[0]?.type === "model.selected" && DateTime.toEpochMillis(logged[0].created)).toBe(
expect(logged.map((item) => item.type)).toEqual(["session.model.selected", "log.synced"])
expect(logged[0]?.type === "session.model.selected" && DateTime.toEpochMillis(logged[0].created)).toBe(
1_717_171_717_000,
)
expect(logged.at(-1)).toEqual(synced)
@ -228,7 +228,7 @@ const modelSwitchedMessage = {
const modelSwitchedEvent = {
id: "evt_model",
created: 1_717_171_717_000,
type: "model.selected",
type: "session.model.selected",
durable: { aggregateID: "ses_test", seq: 1, version: 1 },
data: {
sessionID: "ses_test",

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)
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 () => {
@ -329,7 +329,7 @@ const synced = { type: "log.synced", aggregateID: "ses_test", seq: 1 }
const modelSwitchedEvent = {
id: "evt_model",
created: 1_717_171_717_000,
type: "model.selected",
type: "session.model.selected",
durable: { aggregateID: "ses_test", seq: 1, version: 1 },
data: {
sessionID: "ses_test",

View file

@ -43,5 +43,6 @@ export const migrations = (
import("./migration/20260702134641_add_session_context_entry"),
import("./migration/20260703090000_reset_v2_event_rename_sweep"),
import("./migration/20260703181610_event_created_column"),
import("./migration/20260703190000_reset_v2_shell_event_payloads"),
])
).map((module) => module.default) satisfies DatabaseMigration.Migration[]

View file

@ -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

View file

@ -41,8 +41,8 @@ import type { EventLog } from "@opencode-ai/schema/event-log"
import { SkillV2 } from "./skill"
import { Job } from "./job"
import { CommandV2 } from "./command"
import { Identifier } from "./util/identifier"
import { Shell } from "./shell"
import { Shell as ShellSchema } from "@opencode-ai/schema/shell"
import { KeyedMutex } from "./effect/keyed-mutex"
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({
create: Effect.fn("V2Session.create")(function* (input) {
const sessionID = input.id ?? SessionSchema.ID.create()
@ -550,23 +537,38 @@ const layer = Layer.effect(
Effect.gen(function* () {
activeShells.add(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(
SessionEvent.Shell.Started,
{
sessionID: input.sessionID,
callID,
command: input.command,
shell: started,
},
{ id: input.id },
)
const output = yield* runShellCommand(input.command, session.location.directory).pipe(
Effect.provide(locations.get(session.location)),
)
const completed = yield* Effect.gen(function* () {
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, {
sessionID: input.sessionID,
callID,
output,
shell: completed.shell,
output: completed.output,
})
}).pipe(
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) =>
Prompt.make({
text: input.text,

View file

@ -129,7 +129,7 @@ const serialize = (message: SessionMessage.Message) => {
if (message.type === "system") return `[System update]: ${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 === "shell") return `[Shell]: ${message.command}\n${truncate(message.output)}`
if (message.type === "shell") return `[Shell]: ${message.shell.command}\n${truncate(message.output?.output ?? "")}`
return ""
}

View file

@ -8,21 +8,25 @@ export type MemoryState = {
}
export interface Adapter {
readonly getCurrentAssistant: () => Effect.Effect<SessionMessage.Assistant | undefined>
readonly getAssistant: (messageID: SessionMessage.ID) => Effect.Effect<SessionMessage.Assistant | undefined>
readonly getCurrentShell: (callID: string) => Effect.Effect<SessionMessage.Shell | undefined>
readonly updateAssistant: (assistant: SessionMessage.Assistant) => Effect.Effect<void>
readonly updateShell: (shell: SessionMessage.Shell) => Effect.Effect<void>
readonly appendMessage: (message: SessionMessage.Message) => Effect.Effect<void>
readonly getCurrentAssistant: () => Effect.Effect<SessionMessage.Assistant | undefined, never, never>
readonly getAssistant: (
messageID: SessionMessage.ID,
) => Effect.Effect<SessionMessage.Assistant | undefined, never, never>
readonly getShell: (
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 {
const assistantIndex = (messageID: SessionMessage.ID) =>
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.
const latestAssistantIndex = () => state.messages.findLastIndex((message) => message.type === "assistant")
const activeShellIndex = (callID: string) =>
state.messages.findLastIndex((message) => message.type === "shell" && message.callID === callID)
return {
getCurrentAssistant() {
@ -41,12 +45,11 @@ export function memory(state: MemoryState): Adapter {
return assistant?.type === "assistant" ? assistant : undefined
})
},
getCurrentShell(callID) {
getShell(shellID) {
return Effect.sync(() => {
const index = activeShellIndex(callID)
if (index < 0) return
const shell = state.messages[index]
return shell?.type === "shell" ? shell : undefined
return state.messages.find((message): message is SessionMessage.Shell => {
return message.type === "shell" && message.shell.id === shellID
})
})
},
updateAssistant(assistant) {
@ -60,7 +63,7 @@ export function memory(state: MemoryState): Adapter {
},
updateShell(shell) {
return Effect.sync(() => {
const index = activeShellIndex(shell.callID)
const index = shellIndex(shell.id)
if (index < 0) return
const current = state.messages[index]
if (current?.type !== "shell") return
@ -100,7 +103,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
return Effect.gen(function* () {
yield* SessionEvent.All.match(event, {
"agent.selected": (event) => {
"session.agent.selected": (event) => {
return adapter.appendMessage(
SessionMessage.AgentSelected.make({
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(
SessionMessage.ModelSelected.make({
id: SessionMessage.ID.fromEvent(event.id),
@ -123,11 +126,11 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
)
},
"session.moved": () => Effect.void,
renamed: () => Effect.void,
forked: () => Effect.void,
"prompt.promoted": () => Effect.void,
"prompt.admitted": () => Effect.void,
"execution.settled": () => Effect.void,
"session.renamed": () => Effect.void,
"session.forked": () => Effect.void,
"session.prompt.promoted": () => Effect.void,
"session.prompt.admitted": () => Effect.void,
"session.execution.settled": () => Effect.void,
"session.context.updated": (event) =>
adapter.appendMessage(
SessionMessage.System.make({
@ -137,7 +140,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
time: { created: event.created },
}),
),
synthetic: (event) => {
"session.synthetic": (event) => {
return adapter.appendMessage(
SessionMessage.Synthetic.make({
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(
SessionMessage.Skill.make({
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(
SessionMessage.Shell.make({
id: SessionMessage.ID.fromEvent(event.id),
type: "shell",
metadata: event.metadata,
callID: event.data.callID,
command: event.data.command,
output: "",
shell: event.data.shell,
time: { created: event.created },
}),
)
},
"shell.ended": (event) => {
"session.shell.ended": (event) => {
return Effect.gen(function* () {
const currentShell = yield* adapter.getCurrentShell(event.data.callID)
const currentShell = yield* adapter.getShell(event.data.shell.id)
if (currentShell) {
yield* adapter.updateShell(
produce(currentShell, (draft) => {
draft.shell = castDraft(event.data.shell)
draft.output = event.data.output
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* () {
const currentAssistant = yield* adapter.getCurrentAssistant()
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) => {
draft.time.completed = event.created
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) => {
draft.time.completed = event.created
draft.finish = "error"
draft.error = event.data.error
})
},
"text.started": (event) => {
"session.text.started": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
draft.content.push(
castDraft(SessionMessage.AssistantText.make({ type: "text", id: event.data.textID, text: "" })),
)
})
},
"text.delta": (event) => {
"session.text.delta": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
const match = latestText(draft, event.data.textID)
if (match) match.text += event.data.delta
})
},
"text.ended": (event) => {
"session.text.ended": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
const match = latestText(draft, event.data.textID)
if (match) match.text = event.data.text
})
},
"tool.input.started": (event) => {
"session.tool.input.started": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
draft.content.push(
castDraft(
@ -265,14 +267,14 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
)
})
},
"tool.input.delta": () => Effect.void,
"tool.input.ended": (event) => {
"session.tool.input.delta": () => Effect.void,
"session.tool.input.ended": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
const match = latestTool(draft, event.data.callID)
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) => {
const match = latestTool(draft, event.data.callID)
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) => {
const match = latestTool(draft, event.data.callID)
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) => {
const match = latestTool(draft, event.data.callID)
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) => {
const match = latestTool(draft, event.data.callID)
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) => {
draft.content.push(
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) => {
const match = latestReasoning(draft, event.data.reasoningID)
if (match) match.text += event.data.delta
})
},
"reasoning.ended": (event) => {
"session.reasoning.ended": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
const match = latestReasoning(draft, event.data.reasoningID)
if (match) {
@ -375,10 +377,10 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
}
})
},
retried: () => Effect.void,
"compaction.started": () => Effect.void,
"compaction.delta": () => Effect.void,
"compaction.ended": (event) => {
"session.retried": () => Effect.void,
"session.compaction.started": () => Effect.void,
"session.compaction.delta": () => Effect.void,
"session.compaction.ended": (event) => {
return adapter.appendMessage(
SessionMessage.Compaction.make({
id: SessionMessage.ID.fromEvent(event.id),
@ -391,9 +393,9 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
}),
)
},
"revert.staged": () => Effect.void,
"revert.cleared": () => Effect.void,
"revert.committed": () => Effect.void,
"session.revert.staged": () => Effect.void,
"session.revert.cleared": () => Effect.void,
"session.revert.committed": () => Effect.void,
})
})
}

View file

@ -393,18 +393,25 @@ function run(db: DatabaseService, event: MessageEvent) {
return message.type === "assistant" ? message : undefined
})
},
getCurrentShell(callID) {
getShell(shellID) {
return Effect.gen(function* () {
const rows = yield* db
const row = yield* db
.select()
.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))
.all()
.limit(1)
.get()
.pipe(Effect.orDie)
return rows
.map(decodeRow)
.find((message): message is SessionMessage.Shell => message.type === "shell" && message.callID === callID)
if (!row) return
const message = decodeRow(row)
return message.type === "shell" ? message : undefined
})
},
updateAssistant: updateMessage,

View file

@ -139,7 +139,7 @@ function toLLMMessage(message: SessionMessage.Message, model: Model): Message[]
Message.make({
id: message.id,
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,
}),
]

View file

@ -213,7 +213,7 @@ describe("SessionV2.create", () => {
expect(forkContext.map((message) => message.id)).not.toEqual(parentContext.map((message) => message.id))
expect(history).toHaveLength(1)
expect(history[0]).toMatchObject({
type: "forked",
type: "session.forked",
durable: { seq: 0 },
data: { sessionID: forked.id, parentID: parent.id },
})
@ -378,8 +378,8 @@ describe("SessionV2.create", () => {
expect(
Array.from(yield* logEvents(session, created.id, true).pipe(Stream.take(2), Stream.runCollect)),
).toMatchObject([
{ durable: { seq: 1 }, type: "prompt.admitted", data: { prompt: { text: "Hello" } } },
{ durable: { seq: 2 }, type: "prompt.promoted" },
{ durable: { seq: 1 }, type: "session.prompt.admitted", data: { prompt: { text: "Hello" } } },
{ 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 shell = messages.find((message): message is SessionMessage.Shell => message.type === "shell")
expect(shell).toMatchObject({ type: "shell", command: "echo hello" })
expect(shell?.output).toContain("hello")
expect(shell).toMatchObject({ type: "shell", shell: { command: "echo hello", status: "exited", exit: 0 } })
expect(shell?.output?.output).toContain("hello")
expect(shell?.output?.truncated).toBe(false)
expect(shell?.time.completed).toBeDefined()
}),
),
@ -513,7 +514,8 @@ describe("SessionV2.create", () => {
const messages = yield* session.messages({ sessionID: created.id, order: "asc" })
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()
}),
),
@ -529,7 +531,7 @@ describe("SessionV2.create", () => {
expect(yield* session.get(created.id)).toMatchObject({ agent: "plan" })
expect(
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(
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 } }])
}),
)

View file

@ -40,14 +40,14 @@ describe("SessionV2.log", () => {
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
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 watermark = (yield* events.sequences([created.id])).get(created.id)
// Session creation commits a non-public durable event, so the marker's
// 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 })
}),
)
@ -64,7 +64,7 @@ describe("SessionV2.log", () => {
yield* session.rename({ sessionID: created.id, title: "renamed live" })
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 first = 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 sequences = yield* events.sequences([first.id, second.id])

View file

@ -19,6 +19,7 @@ import { SessionMessageUpdater } from "@opencode-ai/core/session/message-updater
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { SessionExecution } from "@opencode-ai/core/session/execution"
import { SessionInput } from "@opencode-ai/core/session/input"
import { Shell } from "@opencode-ai/schema/shell"
import {
SessionContextCheckpointTable,
SessionInputTable,
@ -257,15 +258,32 @@ describe("SessionProjector", () => {
})
yield* events.publish(SessionEvent.Shell.Started, {
sessionID,
callID: "shell-1",
command: "pwd",
shell: Shell.Info.make({
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, {
sessionID,
callID: "shell-1",
output: "/project",
shell: Shell.Info.make({
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, {
sessionID,
reason: "manual",
@ -320,7 +338,8 @@ describe("SessionProjector", () => {
metadata: { source: "projector-test" },
})
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) },
})
expect(messages.find((message) => message.type === "compaction")).toMatchObject({

View file

@ -256,16 +256,16 @@ describe("SessionV2.prompt", () => {
const streamed = Array.from(yield* Fiber.join(fiber))
expect(streamed.map((event): [number | undefined, string] => [event.durable?.seq, event.type])).toEqual([
[0, "prompt.admitted"],
[1, "prompt.admitted"],
[2, "prompt.promoted"],
[3, "prompt.promoted"],
[0, "session.prompt.admitted"],
[1, "session.prompt.admitted"],
[2, "session.prompt.promoted"],
[3, "session.prompt.promoted"],
])
expect(
Array.from(
yield* publicEvents({ sessionID, after: streamed[0].durable?.seq }).pipe(Stream.take(1), Stream.runCollect),
).map((event): [number | undefined, string] => [event.durable?.seq, event.type]),
).toEqual([[1, "prompt.admitted"]])
).toEqual([[1, "session.prompt.admitted"]])
}),
)

View file

@ -7,6 +7,7 @@ import { SessionMessage } from "@opencode-ai/core/session/message"
import { AgentAttachment, FileAttachment } from "@opencode-ai/core/session/prompt"
import { toLLMMessages } from "@opencode-ai/core/session/runner/to-llm-message"
import { SessionV2 } from "@opencode-ai/core/session"
import { Shell } from "@opencode-ai/schema/shell"
import { DateTime } from "effect"
const created = DateTime.makeUnsafe(0)
@ -87,9 +88,18 @@ describe("toLLMMessages", () => {
SessionMessage.Shell.make({
id: id("shell"),
type: "shell",
callID: "shell-1",
command: "pwd",
output: "/project",
shell: Shell.Info.make({
id: Shell.ID.make("sh_test"),
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 },
}),
SessionMessage.Compaction.make({

View file

@ -193,12 +193,12 @@ describe("SessionRunnerLLM recorded", () => {
.orderBy(EventTable.seq)
.all()).map((event) => event.type),
).toEqual([
"prompt.admitted.1",
"prompt.promoted.1",
"step.started.1",
"text.started.1",
"text.ended.1",
"step.ended.1",
"session.prompt.admitted.1",
"session.prompt.promoted.1",
"session.step.started.1",
"session.text.started.1",
"session.text.ended.1",
"session.step.ended.1",
])
}),
)

View file

@ -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(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()
const serialized = JSON.stringify(success)
expect(serialized.split(base64)).toHaveLength(2)
@ -94,7 +94,7 @@ test("provider-executed success retains its compatibility result", async () => {
const { published, publisher } = capture()
await Effect.runPromise(publisher.publish(LLMEvent.toolCall({ ...call, 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")
})
@ -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 === "tool.failed.1")).toBe(true)
expect(published.some((event) => event.type === "session.tool.success.1")).toBe(false)
expect(published.some((event) => event.type === "session.tool.failed.1")).toBe(true)
})
test("old success event data containing result still decodes", () => {

View file

@ -2985,7 +2985,7 @@ describe("SessionRunnerLLM", () => {
{ type: "user", text: "Interrupt provider" },
{ 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)
}),
)
@ -3029,8 +3029,8 @@ describe("SessionRunnerLLM", () => {
},
])
const eventTypes = yield* recordedEventTypes(sessionID)
expect(eventTypes).toContain("step.failed.1")
expect(eventTypes).not.toContain("step.ended.1")
expect(eventTypes).toContain("session.step.failed.1")
expect(eventTypes).not.toContain("session.step.ended.1")
}),
)

View file

@ -158,14 +158,14 @@ export async function runNonInteractivePrompt(input: Input) {
if (!("sessionID" in event.data) || event.data.sessionID !== input.sessionID) continue
const time = toMillis(event.created)
if (event.type === "prompt.promoted") {
if (event.type === "session.prompt.promoted") {
if (event.data.inputID === messageID) {
promoted = true
continue
}
}
if (
event.type === "execution.settled" &&
event.type === "session.execution.settled" &&
event.data.outcome === "interrupted" &&
(interrupted || permissionRejected || questionRejected || formCancelled)
) {
@ -173,7 +173,7 @@ export async function runNonInteractivePrompt(input: Input) {
}
if (!promoted) continue
if (event.type === "step.started") {
if (event.type === "session.step.started") {
const part: StepStartPart = {
id: partID(event.id),
sessionID: input.sessionID,
@ -189,11 +189,11 @@ export async function runNonInteractivePrompt(input: Input) {
continue
}
if (event.type === "text.started") {
if (event.type === "session.text.started") {
starts.set(event.data.textID, { id: partID(event.id), timestamp: time })
continue
}
if (event.type === "text.ended") {
if (event.type === "session.text.ended") {
const started = starts.get(event.data.textID)
const part: TextPart = {
id: started?.id ?? partID(event.id),
@ -207,11 +207,11 @@ export async function runNonInteractivePrompt(input: Input) {
continue
}
if (event.type === "reasoning.started") {
if (event.type === "session.reasoning.started") {
starts.set(event.data.reasoningID, { id: partID(event.id), timestamp: time })
continue
}
if (event.type === "reasoning.ended" && input.thinking) {
if (event.type === "session.reasoning.ended" && input.thinking) {
const started = starts.get(event.data.reasoningID)
const part: ReasoningPart = {
id: started?.id ?? partID(event.id),
@ -236,7 +236,7 @@ export async function runNonInteractivePrompt(input: Input) {
continue
}
if (event.type === "tool.input.started") {
if (event.type === "session.tool.input.started") {
tools.set(event.data.callID, {
id: partID(event.id),
timestamp: time,
@ -246,12 +246,12 @@ export async function runNonInteractivePrompt(input: Input) {
})
continue
}
if (event.type === "tool.input.ended") {
if (event.type === "session.tool.input.ended") {
const current = tools.get(event.data.callID)
if (current) current.raw = event.data.text
continue
}
if (event.type === "tool.called") {
if (event.type === "session.tool.called") {
const current = tools.get(event.data.callID)
tools.set(event.data.callID, {
id: current?.id ?? partID(event.id),
@ -264,7 +264,7 @@ export async function runNonInteractivePrompt(input: Input) {
})
continue
}
if (event.type === "tool.success") {
if (event.type === "session.tool.success") {
const current = tools.get(event.data.callID) ?? fallbackTool(event)
const part: ToolPart = {
id: current.id,
@ -297,7 +297,7 @@ export async function runNonInteractivePrompt(input: Input) {
if (!emit("tool_use", time, { part })) await input.renderTool(part)
continue
}
if (event.type === "tool.failed") {
if (event.type === "session.tool.failed") {
const current = tools.get(event.data.callID) ?? fallbackTool(event)
const error = event.data.error.message
const part: ToolPart = {
@ -328,7 +328,7 @@ export async function runNonInteractivePrompt(input: Input) {
continue
}
if (event.type === "step.ended") {
if (event.type === "session.step.ended") {
const part: StepFinishPart = {
id: partID(event.id),
sessionID: input.sessionID,
@ -342,14 +342,14 @@ export async function runNonInteractivePrompt(input: Input) {
emit("step_finish", time, { part })
continue
}
if (event.type === "step.failed") {
if (event.type === "session.step.failed") {
if (interrupted || permissionRejected || questionRejected || formCancelled) continue
emittedError = true
process.exitCode = 1
if (!emit("error", time, { error: event.data.error })) UI.error(event.data.error.message)
continue
}
if (event.type === "execution.settled") {
if (event.type === "session.execution.settled") {
if (event.data.outcome === "failure" && !emittedError && !questionRejected && !formCancelled) {
emittedError = true
process.exitCode = 1

View file

@ -62,7 +62,7 @@ type SessionCommit = StreamCommit
// - sent: part ID → byte offset of last flushed text (for incremental output)
// - visible: part ID → rendered text for an active part after display transforms
// - 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
type ShellCall = {
source: "shell" | "tool"
@ -607,12 +607,12 @@ function toolCommit(
}
}
function shellPartID(callID: string): string {
return `shell:${callID}`
function shellPartID(shellID: string): string {
return `shell:${shellID}`
}
function claimShell(data: SessionData, callID: string, source: ShellCall["source"], command?: string): ShellCall {
const current = data.shell.get(callID)
function claimShell(data: SessionData, shellID: string, source: ShellCall["source"], command?: string): ShellCall {
const current = data.shell.get(shellID)
if (current) {
if (command && !current.command) {
current.command = command
@ -625,7 +625,7 @@ function claimShell(data: SessionData, callID: string, source: ShellCall["source
source,
...(command ? { command } : {}),
} satisfies ShellCall
data.shell.set(callID, next)
data.shell.set(shellID, next)
return next
}
@ -728,37 +728,37 @@ export function reduceSessionData(input: SessionDataInput): SessionDataOutput {
const data = input.data
const event = input.event
if (event.type === "shell.started") {
if (event.type === "session.shell.started") {
if (event.properties.sessionID !== input.sessionID) {
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") {
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)) {
return out(data, commits, patch({ status: "running shell" }))
}
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" }))
}
if (event.type === "shell.ended") {
if (event.type === "session.shell.ended") {
if (event.properties.sessionID !== input.sessionID) {
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") {
return out(data, commits)
}
const partID = shellPartID(event.properties.callID)
const partID = shellPartID(event.properties.shell.id)
const seen = data.tools.has(partID)
const command = shell.command ?? ""
data.tools.delete(partID)
@ -767,11 +767,11 @@ export function reduceSessionData(input: SessionDataInput): SessionDataOutput {
}
if (!seen && command) {
commits.push(startShell(event.properties.callID, command))
commits.push(startShell(event.properties.shell.id, command))
}
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)
}

View file

@ -424,21 +424,21 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
}
const reduce = (child: ChildState, event: V2Event) => {
if (event.type === "prompt.promoted") {
if (event.type === "session.prompt.promoted") {
if (userFrame(child, event.data.inputID, "")) {
touch(child, event.created)
notifyDetail(child)
}
return
}
if (event.type === "step.started") {
if (event.type === "session.step.started") {
touch(child, event.created)
if (child.label === FALLBACK_LABEL && event.data.agent) child.label = Locale.titlecase(event.data.agent)
if (child.status !== "running") child.status = "running"
input.emit()
return
}
if (event.type === "text.delta") {
if (event.type === "session.text.delta") {
const projected = child.projectedText.get(event.data.textID)
const covered = projected?.indexOf(event.data.delta) ?? -1
if (projected && covered >= 0) {
@ -459,7 +459,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
notifyDetail(child)
return
}
if (event.type === "text.ended") {
if (event.type === "session.text.ended") {
child.text.set(event.data.textID, event.data.text)
child.projectedText.delete(event.data.textID)
setFrame(child, `text:${event.data.textID}`, {
@ -474,7 +474,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
notifyDetail(child)
return
}
if (event.type === "reasoning.delta") {
if (event.type === "session.reasoning.delta") {
const projected = child.projectedReasoning.get(event.data.reasoningID)
const covered = projected?.indexOf(event.data.delta) ?? -1
if (projected && covered >= 0) {
@ -495,7 +495,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
notifyDetail(child)
return
}
if (event.type === "reasoning.ended") {
if (event.type === "session.reasoning.ended") {
child.reasoning.set(event.data.reasoningID, event.data.text)
child.projectedReasoning.delete(event.data.reasoningID)
if (!input.thinking) return
@ -510,11 +510,11 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
notifyDetail(child)
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 })
return
}
if (event.type === "tool.called") {
if (event.type === "session.tool.called") {
const current = child.tools.get(event.data.callID)
child.tools.set(event.data.callID, {
name: event.data.tool,
@ -537,10 +537,10 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
notifyDetail(child)
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
const current = child.tools.get(event.data.callID)
const failed = event.type === "tool.failed"
const failed = event.type === "session.tool.failed"
childTool(
child,
{
@ -577,7 +577,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
notifyDetail(child)
return
}
if (event.type === "step.failed") {
if (event.type === "session.step.failed") {
setFrame(child, `error:step:${event.data.assistantMessageID}`, {
kind: "error",
source: "system",
@ -589,7 +589,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
notifyDetail(child)
return
}
if (event.type === "execution.settled") {
if (event.type === "session.execution.settled") {
child.status =
event.data.outcome === "success" ? "completed" : event.data.outcome === "interrupted" ? "cancelled" : "error"
touch(child, event.created)
@ -613,15 +613,15 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
return {
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)
return
}
if (event.type === "tool.failed") {
if (event.type === "session.tool.failed") {
pendingCalls.delete(event.data.callID)
return
}
if (event.type !== "tool.success") return
if (event.type !== "session.tool.success") return
const pending = pendingCalls.get(event.data.callID)
pendingCalls.delete(event.data.callID)
const found = childSessionID(record(event.data.structured))

View file

@ -213,7 +213,9 @@ function promptAgents(next: SessionTurnInput) {
? [
{
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
}
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
if (!render) {
// Suppressed history: mark settled shells rendered so live redelivery
// stays silent. A still-running shell stays unmarked and renders in
// full when its live shell.ended event arrives.
if (completed) {
state.shellStarted.add(message.callID)
state.shellEnded.add(message.callID)
state.shellStarted.add(message.shell.id)
state.shellEnded.add(message.shell.id)
}
return
}
if (!state.shellStarted.has(message.callID)) {
state.shellStarted.add(message.callID)
write([shellCommit(message.callID, message.command, { text: "running shell", phase: "start", toolState: "running" })])
if (!state.shellStarted.has(message.shell.id)) {
state.shellStarted.add(message.shell.id)
write([
shellCommit(message.shell.id, message.shell.command, {
text: "running shell",
phase: "start",
toolState: "running",
}),
])
}
if (completed && !state.shellEnded.has(message.callID)) {
state.shellEnded.add(message.callID)
write([shellCommit(message.callID, message.command, { text: message.output, phase: "progress", toolState: "completed" })])
if (completed && message.output && !state.shellEnded.has(message.shell.id)) {
state.shellEnded.add(message.shell.id)
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
}
if (message.type !== "assistant") return
@ -515,17 +529,17 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
}
input.trace?.write("recv.event", 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
state.messageIDs.add(event.data.inputID)
write([], { phase: "running", status: "waiting for assistant" })
return
}
if (event.type === "step.started") {
if (event.type === "session.step.started") {
write([], { phase: "running", status: "assistant responding" })
return
}
if (event.type === "skill.activated") {
if (event.type === "session.skill.activated") {
const messageID = event.id.replace(/^evt_/, "msg_")
if (state.wait) state.wait.promoted = true
if (state.skillMessages.has(messageID)) return
@ -533,38 +547,56 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
write([skillCommit(messageID, event.data.name)])
return
}
if (event.type === "shell.started") {
state.shellCommands.set(event.data.callID, event.data.command)
if (event.type === "session.shell.started") {
state.shellCommands.set(event.data.shell.id, event.data.shell.command)
const wait = state.shellWait
if (wait && wait.callID === undefined) wait.callID = event.data.callID
if (state.shellStarted.has(event.data.callID)) return
state.shellStarted.add(event.data.callID)
write([shellCommit(event.data.callID, event.data.command, { text: "running shell", phase: "start", toolState: "running" })], {
phase: "running",
status: "running shell",
})
if (wait && wait.callID === undefined) wait.callID = event.data.shell.id
if (state.shellStarted.has(event.data.shell.id)) return
state.shellStarted.add(event.data.shell.id)
write(
[
shellCommit(event.data.shell.id, event.data.shell.command, {
text: "running shell",
phase: "start",
toolState: "running",
}),
],
{
phase: "running",
status: "running shell",
},
)
return
}
if (event.type === "shell.ended") {
const command = state.shellCommands.get(event.data.callID) ?? ""
if (event.type === "session.shell.ended") {
const command = state.shellCommands.get(event.data.shell.id) ?? event.data.shell.command
const commits: StreamCommit[] = []
if (!state.shellStarted.has(event.data.callID)) {
state.shellStarted.add(event.data.callID)
if (command) commits.push(shellCommit(event.data.callID, command, { text: "running shell", phase: "start", toolState: "running" }))
if (!state.shellStarted.has(event.data.shell.id)) {
state.shellStarted.add(event.data.shell.id)
if (command)
commits.push(
shellCommit(event.data.shell.id, command, { text: "running shell", phase: "start", toolState: "running" }),
)
}
if (!state.shellEnded.has(event.data.callID)) {
state.shellEnded.add(event.data.callID)
commits.push(shellCommit(event.data.callID, command, { text: event.data.output, phase: "progress", toolState: "completed" }))
if (!state.shellEnded.has(event.data.shell.id)) {
state.shellEnded.add(event.data.shell.id)
commits.push(
shellCommit(event.data.shell.id, command, {
text: event.data.output.output,
phase: "progress",
toolState: "completed",
}),
)
}
const wait = state.shellWait
// An unset callID means shell.started has not been observed yet (event
// 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: "" })
if (owned) wait.resolve()
return
}
if (event.type === "text.delta") {
if (event.type === "session.text.delta") {
const key = streamPartKey(event.data.assistantMessageID, event.data.textID)
const projected = state.projectedText.get(key)
const covered = projected?.indexOf(event.data.delta) ?? -1
@ -586,7 +618,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
])
return
}
if (event.type === "text.ended") {
if (event.type === "session.text.ended") {
const key = streamPartKey(event.data.assistantMessageID, event.data.textID)
const previous = state.text.get(key) ?? ""
if (event.data.text.length > previous.length)
@ -604,7 +636,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
state.projectedText.delete(key)
return
}
if (event.type === "reasoning.delta") {
if (event.type === "session.reasoning.delta") {
const key = streamPartKey(event.data.assistantMessageID, event.data.reasoningID)
const projected = state.projectedReasoning.get(key)
const covered = projected?.indexOf(event.data.delta) ?? -1
@ -627,7 +659,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
])
return
}
if (event.type === "reasoning.ended") {
if (event.type === "session.reasoning.ended") {
const key = streamPartKey(event.data.assistantMessageID, event.data.reasoningID)
const previous = state.reasoning.get(key) ?? ""
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)
return
}
if (event.type === "tool.input.started") {
if (event.type === "session.tool.input.started") {
state.tools.set(event.data.callID, {
messageID: event.data.assistantMessageID,
name: event.data.name,
@ -655,7 +687,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
})
return
}
if (event.type === "tool.called") {
if (event.type === "session.tool.called") {
if (state.finishedTools.has(event.data.callID)) return
const current = state.tools.get(event.data.callID)
const item: SessionMessageAssistantTool = {
@ -669,10 +701,10 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
renderTool(event.data.assistantMessageID, item)
return
}
if (event.type === "tool.progress") return
if (event.type === "tool.success" || event.type === "tool.failed") {
if (event.type === "session.tool.progress") return
if (event.type === "session.tool.success" || event.type === "session.tool.failed") {
const current = state.tools.get(event.data.callID)
const failed = event.type === "tool.failed"
const failed = event.type === "session.tool.failed"
const item: SessionMessageAssistantTool = {
type: "tool",
id: event.data.callID,
@ -720,7 +752,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
syncBlockers()
return
}
if (event.type === "step.ended") {
if (event.type === "session.step.ended") {
const total =
event.data.tokens.input +
event.data.tokens.output +
@ -734,13 +766,13 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
})
return
}
if (event.type === "step.failed") {
if (event.type === "session.step.failed") {
state.errors.add(event.data.assistantMessageID)
if (state.wait) state.wait.failureRendered = true
write([{ kind: "error", source: "system", text: errorMessage(event.data.error), phase: "start" }])
return
}
if (event.type === "execution.settled") {
if (event.type === "session.execution.settled") {
write([], { phase: "idle", status: "" })
const current = state.wait
if (!current || (!current.promoted && !current.interrupted)) return
@ -977,10 +1009,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
sessionID: input.sessionID,
id: messageID,
prompt: {
text: [
next.prompt.text,
...prepared.flatMap((file) => (file.text ? [file.text] : [])),
].join("\n\n"),
text: [next.prompt.text, ...prepared.flatMap((file) => (file.text ? [file.text] : []))].join("\n\n"),
files: attachments.length ? attachments : undefined,
agents: agents.length ? agents : undefined,
},

View file

@ -25,7 +25,7 @@ function prompted(inputID: string): V2Event {
return {
id: "evt_prompted",
created: 0,
type: "prompt.promoted",
type: "session.prompt.promoted",
durable: { aggregateID: "ses_1", seq: 0, version: 1 },
data: { sessionID: "ses_1", inputID },
}
@ -35,7 +35,7 @@ function settled(outcome: "success" | "interrupted" = "success"): V2Event {
return {
id: "evt_settled",
created: 0,
type: "execution.settled",
type: "session.execution.settled",
data: { sessionID: "ses_1", outcome },
}
}

View file

@ -329,7 +329,7 @@ describe("run session data", () => {
test("renders direct shell mode from first-class shell events", () => {
let data = createSessionData()
const started = reduce(data, {
type: "shell.started",
type: "session.shell.started",
properties: {
sessionID: "session-1",
timestamp: 1,
@ -353,7 +353,7 @@ describe("run session data", () => {
data = started.data
const ended = reduce(data, {
type: "shell.ended",
type: "session.shell.ended",
properties: {
sessionID: "session-1",
timestamp: 2,
@ -380,7 +380,7 @@ describe("run session data", () => {
test("suppresses legacy bash part updates once shell events claim the call", () => {
let data = reduce(createSessionData(), {
type: "shell.started",
type: "session.shell.started",
properties: {
sessionID: "session-1",
timestamp: 1,
@ -409,7 +409,7 @@ describe("run session data", () => {
).toEqual([])
data = reduce(data, {
type: "shell.ended",
type: "session.shell.ended",
properties: {
sessionID: "session-1",
timestamp: 2,
@ -463,7 +463,7 @@ describe("run session data", () => {
expect(
reduce(data, {
type: "shell.started",
type: "session.shell.started",
properties: {
sessionID: "session-1",
timestamp: 1,
@ -497,7 +497,7 @@ describe("run session data", () => {
expect(
reduce(data, {
type: "shell.ended",
type: "session.shell.ended",
properties: {
sessionID: "session-1",
timestamp: 2,

View file

@ -200,7 +200,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_prompted",
created: 0,
type: "prompt.promoted",
type: "session.prompt.promoted",
durable: durable("ses_1"),
data: {
sessionID: "ses_1",
@ -210,7 +210,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_text",
created: 0,
type: "text.delta",
type: "session.text.delta",
data: {
sessionID: "ses_1",
assistantMessageID: "msg_assistant",
@ -221,7 +221,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_settled",
created: 0,
type: "execution.settled",
type: "session.execution.settled",
data: { sessionID: "ses_1", outcome: "success" },
})
await turn
@ -259,7 +259,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_prompted",
created: 0,
type: "prompt.promoted",
type: "session.prompt.promoted",
durable: durable("ses_1"),
data: {
sessionID: "ses_1",
@ -269,7 +269,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_settled",
created: 0,
type: "execution.settled",
type: "session.execution.settled",
data: { sessionID: "ses_1", outcome: "success" },
})
})
@ -353,7 +353,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_prompted",
created: 0,
type: "prompt.promoted",
type: "session.prompt.promoted",
durable: durable("ses_1"),
data: {
sessionID: "ses_1",
@ -363,7 +363,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_settled",
created: 0,
type: "execution.settled",
type: "session.execution.settled",
data: { sessionID: "ses_1", outcome: "success" },
})
})
@ -450,7 +450,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_prompted",
created: 0,
type: "prompt.promoted",
type: "session.prompt.promoted",
durable: durable("ses_1"),
data: {
sessionID: "ses_1",
@ -460,7 +460,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_settled",
created: 0,
type: "execution.settled",
type: "session.execution.settled",
data: { sessionID: "ses_1", outcome: "success" },
})
})
@ -724,7 +724,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_text",
created: 0,
type: "text.delta",
type: "session.text.delta",
data: {
sessionID: "ses_1",
assistantMessageID: "msg_assistant",
@ -809,7 +809,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_reasoning",
created: 0,
type: "reasoning.ended",
type: "session.reasoning.ended",
durable: durable("ses_1"),
data: {
sessionID: "ses_1",
@ -865,7 +865,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_settled",
created: 0,
type: "execution.settled",
type: "session.execution.settled",
data: { sessionID: "ses_1", outcome: "success" },
})
await turn
@ -921,7 +921,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_prompted",
created: 0,
type: "prompt.promoted",
type: "session.prompt.promoted",
durable: durable("ses_1"),
data: {
sessionID: "ses_1",
@ -931,7 +931,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_settled",
created: 0,
type: "execution.settled",
type: "session.execution.settled",
data: { sessionID: "ses_1", outcome: "success" },
})
await turn
@ -981,7 +981,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_prompted",
created: 0,
type: "prompt.promoted",
type: "session.prompt.promoted",
durable: durable("ses_1"),
data: {
sessionID: "ses_1",
@ -993,7 +993,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_settled",
created: 0,
type: "execution.settled",
type: "session.execution.settled",
data: { sessionID: "ses_1", outcome: "success" },
})
await turn
@ -1021,16 +1021,42 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_shell_start",
created: 0,
type: "shell.started",
type: "session.shell.started",
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({
id: "evt_shell_end",
created: 0,
type: "shell.ended",
type: "session.shell.ended",
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
@ -1047,8 +1073,8 @@ describe("V2 mini transport", () => {
expect(request).toMatchObject({ sessionID: "ses_1", command: "ls" })
expect(ui.commits.filter((item) => item.shell)).toMatchObject([
{ phase: "start", tool: "bash", toolState: "running", shell: { callID: "call_shell", command: "ls" } },
{ phase: "progress", text: "file.txt", toolState: "completed", 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: "sh_shell", command: "ls" } },
])
expect(ui.events).toContainEqual({ type: "stream.patch", patch: { phase: "running", status: "running shell" } })
await transport.close()
@ -1107,9 +1133,18 @@ describe("V2 mini transport", () => {
{
id: "msg_shell",
type: "shell" as const,
callID: "call_1",
command: "ls",
output: "file.txt",
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 },
time: { created: 1, completed: 2 },
},
],
@ -1127,15 +1162,29 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_shell_end",
created: 0,
type: "shell.ended",
type: "session.shell.ended",
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)
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" },
])
await transport.close()
@ -1160,7 +1209,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_prompted",
created: 0,
type: "prompt.promoted",
type: "session.prompt.promoted",
durable: durable("ses_1"),
data: {
sessionID: "ses_1",
@ -1170,7 +1219,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_settled",
created: 0,
type: "execution.settled",
type: "session.execution.settled",
data: { sessionID: "ses_1", outcome: "success" },
})
})
@ -1236,7 +1285,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_skill",
created: 0,
type: "skill.activated",
type: "session.skill.activated",
durable: durable("ses_1"),
data: {
sessionID: "ses_1",
@ -1247,7 +1296,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_settled",
created: 0,
type: "execution.settled",
type: "session.execution.settled",
data: { sessionID: "ses_1", outcome: "success" },
})
})
@ -1317,7 +1366,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_unrelated_settled",
created: 0,
type: "execution.settled",
type: "session.execution.settled",
data: { sessionID: "ses_1", outcome: "success" },
})
await Bun.sleep(0)
@ -1327,7 +1376,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_skill",
created: 0,
type: "skill.activated",
type: "session.skill.activated",
durable: durable("ses_1"),
data: {
sessionID: "ses_1",
@ -1338,7 +1387,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_skill_settled",
created: 0,
type: "execution.settled",
type: "session.execution.settled",
data: { sessionID: "ses_1", outcome: "success" },
})
await turn
@ -1376,7 +1425,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_skill",
created: 0,
type: "skill.activated",
type: "session.skill.activated",
durable: durable("ses_1"),
data: {
sessionID: "ses_1",
@ -1438,7 +1487,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_child_step",
created: 0,
type: "step.started",
type: "session.step.started",
durable: durable("ses_child"),
data: {
sessionID: "ses_child",
@ -1456,7 +1505,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_child_text",
created: 0,
type: "text.delta",
type: "session.text.delta",
data: {
sessionID: "ses_child",
assistantMessageID: "msg_child_a",
@ -1470,7 +1519,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_child_settled",
created: 0,
type: "execution.settled",
type: "session.execution.settled",
data: { sessionID: "ses_child", outcome: "success" },
})
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({
id: "evt_child_step",
created: 0,
type: "step.started",
type: "session.step.started",
durable: durable("ses_child"),
data: {
sessionID: "ses_child",
@ -1527,7 +1576,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_child_settled",
created: 0,
type: "execution.settled",
type: "session.execution.settled",
data: { sessionID: "ses_child", outcome: "interrupted" },
})
await Bun.sleep(0)
@ -1574,7 +1623,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_child_step",
created: 0,
type: "step.started",
type: "session.step.started",
durable: durable("ses_child"),
data: {
sessionID: "ses_child",
@ -1587,7 +1636,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_parent_call",
created: 0,
type: "tool.called",
type: "session.tool.called",
durable: durable("ses_1"),
data: {
sessionID: "ses_1",
@ -1601,7 +1650,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_parent_success",
created: 0,
type: "tool.success",
type: "session.tool.success",
durable: durable("ses_1", 1),
data: {
sessionID: "ses_1",
@ -1616,7 +1665,7 @@ describe("V2 mini transport", () => {
events.push({
id: "evt_child_settled",
created: 0,
type: "execution.settled",
type: "session.execution.settled",
data: { sessionID: "ses_child", outcome: "interrupted" },
})
while (!states().some((state) => state.tabs.some((tab) => tab.status === "cancelled"))) await Bun.sleep(0)

View file

@ -99,10 +99,10 @@ describe("pty HttpApi bridge", () => {
const updated = await app().request(PtyPaths.update.replace(":ptyID", info.id), {
method: "PUT",
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(await updated.json()).toMatchObject({ id: info.id, title: "renamed" })
expect(await updated.json()).toMatchObject({ id: info.id, title: "session.renamed" })
} finally {
await app().request(PtyPaths.remove.replace(":ptyID", info.id), { method: "DELETE", headers })
}

View file

@ -573,7 +573,7 @@ describe("HttpApi SDK", () => {
const child = yield* capture(() => sdk.session.create({ title: "child", parentID }))
const childID = String(record(child.data).id)
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 all = yield* capture(() => sdk.session.list({ roots: false, limit: 10 }))
const children = yield* capture(() => sdk.session.children({ sessionID: parentID }))

View file

@ -271,7 +271,7 @@ describe("tool.apply_patch freeform", () => {
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)
expect(yield* readText(moved)).toBe("new content\n")
}),
@ -282,7 +282,7 @@ describe("tool.apply_patch freeform", () => {
const test = yield* TestInstance
const { ctx } = makeCtx()
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(destination))
yield* writeText(original, "from\n")

View file

@ -79593,8 +79593,8 @@
}
}
},
"synthetic": {
"id": "synthetic",
"session.synthetic": {
"id": "session.synthetic",
"env": ["SYNTHETIC_API_KEY"],
"npm": "@ai-sdk/openai-compatible",
"api": "https://api.synthetic.new/openai/v1",

View file

@ -22,7 +22,7 @@ test.skip("step snapshots carry over to assistant messages", () => {
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: "step.started",
type: "session.step.started",
durable: durable(sessionID),
data: {
sessionID,
@ -44,7 +44,7 @@ test.skip("step snapshots carry over to assistant messages", () => {
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: "step.ended",
type: "session.step.ended",
durable: durable(sessionID, 1, 2),
data: {
sessionID,
@ -77,7 +77,7 @@ test.skip("text ended populates assistant text content", () => {
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: "step.started",
type: "session.step.started",
durable: durable(sessionID),
data: {
sessionID,
@ -96,7 +96,7 @@ test.skip("text ended populates assistant text content", () => {
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: "text.started",
type: "session.text.started",
durable: durable(sessionID, 1),
data: {
sessionID,
@ -110,7 +110,7 @@ test.skip("text ended populates assistant text content", () => {
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: "text.ended",
type: "session.text.ended",
durable: durable(sessionID, 2),
data: {
sessionID,
@ -136,7 +136,7 @@ test.skip("tool completion stores completed timestamp", () => {
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: "step.started",
type: "session.step.started",
durable: durable(sessionID),
data: {
sessionID,
@ -155,7 +155,7 @@ test.skip("tool completion stores completed timestamp", () => {
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: "tool.input.started",
type: "session.tool.input.started",
durable: durable(sessionID, 1),
data: {
sessionID,
@ -170,7 +170,7 @@ test.skip("tool completion stores completed timestamp", () => {
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: "tool.called",
type: "session.tool.called",
durable: durable(sessionID, 2),
data: {
sessionID,
@ -187,7 +187,7 @@ test.skip("tool completion stores completed timestamp", () => {
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: "tool.success",
type: "session.tool.success",
durable: durable(sessionID, 3),
data: {
sessionID,
@ -218,7 +218,7 @@ test("compaction events reduce to compaction message only when completed", () =>
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id,
created: DateTime.makeUnsafe(0),
type: "compaction.started",
type: "session.compaction.started",
durable: durable(sessionID),
data: {
sessionID,
@ -233,7 +233,7 @@ test("compaction events reduce to compaction message only when completed", () =>
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: "compaction.delta",
type: "session.compaction.delta",
data: {
sessionID,
text: "hello ",
@ -245,7 +245,7 @@ test("compaction events reduce to compaction message only when completed", () =>
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: EventV2.ID.create(),
created: DateTime.makeUnsafe(0),
type: "compaction.delta",
type: "session.compaction.delta",
data: {
sessionID,
text: "summary",
@ -257,7 +257,7 @@ test("compaction events reduce to compaction message only when completed", () =>
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
id: endedID,
created: DateTime.makeUnsafe(0),
type: "compaction.ended",
type: "session.compaction.ended",
durable: durable(sessionID, 1),
data: {
sessionID,

View file

@ -12,6 +12,7 @@ import { SessionID } from "./session-id.js"
import { Location } from "./location.js"
import { SessionMessage } from "./session-message.js"
import { Revert } from "./revert.js"
import { Shell as ShellSchema } from "./shell.js"
export { FileAttachment }
@ -51,7 +52,7 @@ export const UnknownError = SessionMessage.UnknownError
export type UnknownError = SessionMessage.UnknownError
export const AgentSelected = Event.durable({
type: "agent.selected",
type: "session.agent.selected",
...options,
schema: {
...Base,
@ -61,7 +62,7 @@ export const AgentSelected = Event.durable({
export type AgentSelected = typeof AgentSelected.Type
export const ModelSelected = Event.durable({
type: "model.selected",
type: "session.model.selected",
...options,
schema: {
...Base,
@ -82,7 +83,7 @@ export const Moved = Event.durable({
export type Moved = typeof Moved.Type
export const Renamed = Event.durable({
type: "renamed",
type: "session.renamed",
...options,
schema: {
...Base,
@ -92,7 +93,7 @@ export const Renamed = Event.durable({
export type Renamed = typeof Renamed.Type
export const Forked = Event.durable({
type: "forked",
type: "session.forked",
...options,
schema: {
...Base,
@ -103,7 +104,7 @@ export const Forked = Event.durable({
export type Forked = typeof Forked.Type
export const PromptPromoted = Event.durable({
type: "prompt.promoted",
type: "session.prompt.promoted",
...options,
schema: {
sessionID: SessionID,
@ -113,14 +114,14 @@ export const PromptPromoted = Event.durable({
export type PromptPromoted = typeof PromptPromoted.Type
export const PromptAdmitted = Event.durable({
type: "prompt.admitted",
type: "session.prompt.admitted",
...options,
schema: PromptFields,
})
export type PromptAdmitted = typeof PromptAdmitted.Type
export const ExecutionSettled = Event.ephemeral({
type: "execution.settled",
type: "session.execution.settled",
schema: {
...Base,
outcome: Schema.Literals(["success", "failure", "interrupted"]),
@ -140,7 +141,7 @@ export const ContextUpdated = Event.durable({
export type ContextUpdated = typeof ContextUpdated.Type
export const Synthetic = Event.durable({
type: "synthetic",
type: "session.synthetic",
...options,
schema: {
...Base,
@ -153,7 +154,7 @@ export type Synthetic = typeof Synthetic.Type
export namespace Skill {
export const Activated = Event.durable({
type: "skill.activated",
type: "session.skill.activated",
...options,
schema: {
...Base,
@ -166,23 +167,22 @@ export namespace Skill {
export namespace Shell {
export const Started = Event.durable({
type: "shell.started",
type: "session.shell.started",
...options,
schema: {
...Base,
callID: Schema.String,
command: Schema.String,
shell: ShellSchema.Info,
},
})
export type Started = typeof Started.Type
export const Ended = Event.durable({
type: "shell.ended",
type: "session.shell.ended",
...options,
schema: {
...Base,
callID: Schema.String,
output: Schema.String,
shell: ShellSchema.Info,
output: ShellSchema.Output,
},
})
export type Ended = typeof Ended.Type
@ -190,7 +190,7 @@ export namespace Shell {
export namespace Step {
export const Started = Event.durable({
type: "step.started",
type: "session.step.started",
...options,
schema: {
...Base,
@ -203,7 +203,7 @@ export namespace Step {
export type Started = typeof Started.Type
export const Ended = Event.durable({
type: "step.ended",
type: "session.step.ended",
...stepSettlementOptions,
schema: {
...Base,
@ -226,7 +226,7 @@ export namespace Step {
export type Ended = typeof Ended.Type
export const Failed = Event.durable({
type: "step.failed",
type: "session.step.failed",
...stepSettlementOptions,
schema: {
...Base,
@ -239,7 +239,7 @@ export namespace Step {
export namespace Text {
export const Started = Event.durable({
type: "text.started",
type: "session.text.started",
...options,
schema: {
...Base,
@ -251,7 +251,7 @@ export namespace Text {
// Stream fragments are live-only; Text.Ended is the replayable full-value boundary.
export const Delta = Event.ephemeral({
type: "text.delta",
type: "session.text.delta",
schema: {
...Base,
assistantMessageID: SessionMessage.ID,
@ -262,7 +262,7 @@ export namespace Text {
export type Delta = typeof Delta.Type
export const Ended = Event.durable({
type: "text.ended",
type: "session.text.ended",
...options,
schema: {
...Base,
@ -276,7 +276,7 @@ export namespace Text {
export namespace Reasoning {
export const Started = Event.durable({
type: "reasoning.started",
type: "session.reasoning.started",
...options,
schema: {
...Base,
@ -289,7 +289,7 @@ export namespace Reasoning {
// Stream fragments are live-only; Reasoning.Ended is the replayable full-value boundary.
export const Delta = Event.ephemeral({
type: "reasoning.delta",
type: "session.reasoning.delta",
schema: {
...Base,
assistantMessageID: SessionMessage.ID,
@ -300,7 +300,7 @@ export namespace Reasoning {
export type Delta = typeof Delta.Type
export const Ended = Event.durable({
type: "reasoning.ended",
type: "session.reasoning.ended",
...options,
schema: {
...Base,
@ -322,7 +322,7 @@ export namespace Tool {
export namespace Input {
export const Started = Event.durable({
type: "tool.input.started",
type: "session.tool.input.started",
...options,
schema: {
...ToolBase,
@ -333,7 +333,7 @@ export namespace Tool {
// Stream fragments are live-only; Input.Ended is the replayable raw-input boundary.
export const Delta = Event.ephemeral({
type: "tool.input.delta",
type: "session.tool.input.delta",
schema: {
...ToolBase,
delta: Schema.String,
@ -342,7 +342,7 @@ export namespace Tool {
export type Delta = typeof Delta.Type
export const Ended = Event.durable({
type: "tool.input.ended",
type: "session.tool.input.ended",
...options,
schema: {
...ToolBase,
@ -353,7 +353,7 @@ export namespace Tool {
}
export const Called = Event.durable({
type: "tool.called",
type: "session.tool.called",
...options,
schema: {
...ToolBase,
@ -372,7 +372,7 @@ export namespace Tool {
* transitions or at a bounded cadence, not persist every stdout/stderr chunk.
*/
export const Progress = Event.durable({
type: "tool.progress",
type: "session.tool.progress",
...options,
schema: {
...ToolBase,
@ -383,7 +383,7 @@ export namespace Tool {
export type Progress = typeof Progress.Type
export const Success = Event.durable({
type: "tool.success",
type: "session.tool.success",
...options,
schema: {
...ToolBase,
@ -400,7 +400,7 @@ export namespace Tool {
export type Success = typeof Success.Type
export const Failed = Event.durable({
type: "tool.failed",
type: "session.tool.failed",
...options,
schema: {
...ToolBase,
@ -428,7 +428,7 @@ export const RetryError = Schema.Struct({
export interface RetryError extends Schema.Schema.Type<typeof RetryError> {}
export const Retried = Event.durable({
type: "retried",
type: "session.retried",
...options,
schema: {
...Base,
@ -440,7 +440,7 @@ export type Retried = typeof Retried.Type
export namespace Compaction {
export const Started = Event.durable({
type: "compaction.started",
type: "session.compaction.started",
...options,
schema: {
...Base,
@ -450,7 +450,7 @@ export namespace Compaction {
export type Started = typeof Started.Type
export const Delta = Event.ephemeral({
type: "compaction.delta",
type: "session.compaction.delta",
schema: {
...Base,
text: Schema.String,
@ -459,7 +459,7 @@ export namespace Compaction {
export type Delta = typeof Delta.Type
export const Ended = Event.durable({
type: "compaction.ended",
type: "session.compaction.ended",
...options,
schema: {
...Base,
@ -473,13 +473,13 @@ export namespace Compaction {
export namespace RevertEvent {
export const Staged = Event.durable({
type: "revert.staged",
type: "session.revert.staged",
...options,
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({
type: "revert.committed",
type: "session.revert.committed",
...options,
schema: { ...Base, messageID: SessionMessage.ID },
})

View file

@ -9,6 +9,7 @@ import { DateTimeUtcFromMillis, RelativePath, statics } from "./schema.js"
import { SessionID } from "./session-id.js"
import { ascending } from "./identifier.js"
import { Event } from "./event.js"
import { Shell as ShellSchema } from "./shell.js"
export const ID = Schema.String.check(Schema.isStartsWith("msg_")).pipe(
Schema.brand("Session.Message.ID"),
@ -82,9 +83,8 @@ export interface Shell extends Schema.Schema.Type<typeof Shell> {}
export const Shell = Schema.Struct({
...Base,
type: Schema.Literal("shell"),
callID: Schema.String,
command: Schema.String,
output: Schema.String,
shell: ShellSchema.Info,
output: ShellSchema.Output.pipe(optional),
time: Schema.Struct({
created: DateTimeUtcFromMillis,
completed: DateTimeUtcFromMillis.pipe(optional),

View file

@ -53,7 +53,7 @@ describe("public event manifest", () => {
expect(Session.Event.Definitions).toBe(SessionEvent.Definitions)
expect(Workspace.Event).toBe(WorkspaceEvent)
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("agent.updated")).toBe(Agent.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.Error,
])
expect(EventManifest.Durable.get("step.ended.1")).toBe(SessionEvent.Step.Ended)
expect(EventManifest.Durable.has("step.ended.2")).toBe(false)
expect(EventManifest.Durable.get("session.step.ended.1")).toBe(SessionEvent.Step.Ended)
expect(EventManifest.Durable.has("session.step.ended.2")).toBe(false)
})
test("derives durable definitions from explicit definition durability", () => {
@ -88,37 +88,37 @@ describe("public event manifest", () => {
"message.removed.1",
"message.part.updated.1",
"message.part.removed.1",
"agent.selected.1",
"model.selected.1",
"session.agent.selected.1",
"session.model.selected.1",
"session.moved.1",
"renamed.1",
"forked.1",
"prompt.promoted.1",
"prompt.admitted.1",
"session.renamed.1",
"session.forked.1",
"session.prompt.promoted.1",
"session.prompt.admitted.1",
"session.context.updated.1",
"synthetic.1",
"skill.activated.1",
"shell.started.1",
"shell.ended.1",
"step.started.1",
"step.ended.1",
"step.failed.1",
"text.started.1",
"text.ended.1",
"tool.input.started.1",
"tool.input.ended.1",
"tool.called.1",
"tool.progress.1",
"tool.success.1",
"tool.failed.1",
"reasoning.started.1",
"reasoning.ended.1",
"retried.1",
"compaction.started.1",
"compaction.ended.1",
"revert.staged.1",
"revert.cleared.1",
"revert.committed.1",
"session.synthetic.1",
"session.skill.activated.1",
"session.shell.started.1",
"session.shell.ended.1",
"session.step.started.1",
"session.step.ended.1",
"session.step.failed.1",
"session.text.started.1",
"session.text.ended.1",
"session.tool.input.started.1",
"session.tool.input.ended.1",
"session.tool.called.1",
"session.tool.progress.1",
"session.tool.success.1",
"session.tool.failed.1",
"session.reasoning.started.1",
"session.reasoning.ended.1",
"session.retried.1",
"session.compaction.started.1",
"session.compaction.ended.1",
"session.revert.staged.1",
"session.revert.cleared.1",
"session.revert.committed.1",
].toSorted(),
)
expect(SessionEvent.DurableDefinitions).toEqual(

View file

@ -78,7 +78,7 @@ it.live(
prompt: fixture.sdk.Prompt.make({ text: "Promote this input" }),
})
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,
Effect.timeout("10 seconds"),
Effect.map(Option.getOrThrow),
@ -119,7 +119,7 @@ it.live(
expect(page.data.some((session) => session.id === id)).toBe(true)
expect(active).toEqual({ data: {}, watermarks: {} })
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(contextEntries).toEqual([
{ key: "deploy-target", value: "production" },
@ -127,7 +127,7 @@ it.live(
])
expect(remainingContextEntries).toEqual([{ key: "deploy-target", value: "production" }])
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(missing.map((error) => error._tag)).toEqual([
"SessionNotFoundError",
@ -149,13 +149,13 @@ it.live(
const opencode = yield* fixture.sdk.OpenCode.create()
const id = sessionID(fixture)
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(
Stream.runForEach((event) =>
event.type === "server.connected"
? 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)
: Effect.void,
),
@ -191,7 +191,7 @@ it.live(
Stream.runForEach((notification: OpenCodeEvent) =>
notification.type === "server.connected"
? ready.open
: notification.type === "agent.selected" && notification.data.sessionID === id
: notification.type === "session.agent.selected" && notification.data.sessionID === id
? event.open
: Effect.void,
)

View file

@ -60,7 +60,7 @@ if (schemas) {
visit({ ...document, components: { ...document.components, schemas: undefined } })
for (const name of Object.keys(schemas)) {
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,
) &&
!reachable.has(name)
@ -100,7 +100,7 @@ await createClient({
const generatedTypesPath = "./src/v2/gen/types.gen.ts"
const generatedTypes = await Bun.file(generatedTypesPath).text()
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,
)
) {

File diff suppressed because it is too large Load diff

View file

@ -16410,7 +16410,7 @@
"text": {
"type": "string"
},
"synthetic": {
"session.synthetic": {
"type": "boolean"
},
"ignored": {
@ -23071,7 +23071,7 @@
"text": {
"type": "string"
},
"synthetic": {
"session.synthetic": {
"type": "boolean"
},
"ignored": {
@ -27479,7 +27479,7 @@
},
"type": {
"type": "string",
"enum": ["synthetic"]
"enum": ["session.synthetic"]
}
},
"required": ["id", "time", "sessionID", "text", "type"],

View file

@ -126,8 +126,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
const item = position === undefined ? undefined : messages[position]
return item?.type === "assistant" ? item : undefined
},
activeShell(messages: SessionMessage[], callID: string) {
const item = messages.findLast((item) => item.type === "shell" && item.callID === callID)
shell(messages: SessionMessage[], shellID: string) {
const item = messages.findLast((item) => item.type === "shell" && item.shell.id === shellID)
return item?.type === "shell" ? item : undefined
},
latestTool(assistant: SessionMessageAssistant | undefined, callID?: string) {
@ -220,7 +220,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
case "skill.updated":
void result.location.skill.refresh(event.location)
break
case "agent.selected":
case "session.agent.selected":
if (store.session.info[event.data.sessionID])
setStore("session", "info", event.data.sessionID, "agent", event.data.agent)
message.update(event.data.sessionID, (draft, index) => {
@ -232,7 +232,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
})
break
case "model.selected":
case "session.model.selected":
if (store.session.info[event.data.sessionID])
setStore("session", "info", event.data.sessionID, "model", event.data.model)
message.update(event.data.sessionID, (draft, index) => {
@ -244,11 +244,11 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
})
break
case "renamed":
case "session.renamed":
if (store.session.info[event.data.sessionID])
setStore("session", "info", event.data.sessionID, "title", event.data.title)
break
case "prompt.promoted": {
case "session.prompt.promoted": {
setStore("session", "status", event.data.sessionID, "running")
message.update(event.data.sessionID, (draft, index) => {
const position = index.get(event.data.inputID)
@ -264,7 +264,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
break
}
case "prompt.admitted":
case "session.prompt.admitted":
message.update(event.data.sessionID, (draft, index) => {
message.append(draft, index, {
id: event.data.inputID,
@ -287,7 +287,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
})
break
case "synthetic":
case "session.synthetic":
message.update(event.data.sessionID, (draft, index) => {
message.append(draft, index, {
id: messageIDFromEvent(event.id),
@ -299,29 +299,28 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
})
break
case "shell.started":
case "session.shell.started":
setStore("session", "status", event.data.sessionID, "running")
message.update(event.data.sessionID, (draft, index) => {
message.append(draft, index, {
id: messageIDFromEvent(event.id),
type: "shell",
callID: event.data.callID,
command: event.data.command,
output: "",
shell: event.data.shell,
time: { created: event.created },
})
})
break
case "shell.ended":
case "session.shell.ended":
setStore("session", "status", event.data.sessionID, "idle")
message.update(event.data.sessionID, (draft, index) => {
const match = message.activeShell(draft, event.data.callID)
message.update(event.data.sessionID, (draft) => {
const match = message.shell(draft, event.data.shell.id)
if (!match) return
match.shell = event.data.shell
match.output = event.data.output
match.time.completed = event.created
})
break
case "step.started":
case "session.step.started":
setStore("session", "status", event.data.sessionID, "running")
message.update(event.data.sessionID, (draft, index) => {
if (index.has(event.data.assistantMessageID)) return
@ -338,7 +337,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
})
break
case "step.ended":
case "session.step.ended":
setStore("session", "status", event.data.sessionID, "running")
message.update(event.data.sessionID, (draft, index) => {
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 }
})
break
case "step.failed":
case "session.step.failed":
message.update(event.data.sessionID, (draft, index) => {
const currentAssistant = message.assistant(draft, index, event.data.assistantMessageID)
if (!currentAssistant) return
@ -360,7 +359,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
currentAssistant.error = event.data.error
})
break
case "text.started":
case "session.text.started":
message.update(event.data.sessionID, (draft, index) => {
message.assistant(draft, index, event.data.assistantMessageID)?.content.push({
type: "text",
@ -369,7 +368,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
})
break
case "text.delta":
case "session.text.delta":
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestText(
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
})
break
case "text.ended":
case "session.text.ended":
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestText(
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
})
break
case "tool.input.started":
case "session.tool.input.started":
message.update(event.data.sessionID, (draft, index) => {
message.assistant(draft, index, event.data.assistantMessageID)?.content.push({
type: "tool",
@ -398,7 +397,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
})
break
case "tool.input.delta":
case "session.tool.input.delta":
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestTool(
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
})
break
case "tool.input.ended":
case "session.tool.input.ended":
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestTool(
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
})
break
case "tool.called":
case "session.tool.called":
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestTool(
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: [] }
})
break
case "tool.progress":
case "session.tool.progress":
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestTool(
message.assistant(draft, index, event.data.assistantMessageID),
@ -439,7 +438,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
match.state.content = [...event.data.content]
})
break
case "tool.success":
case "session.tool.success":
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestTool(
message.assistant(draft, index, event.data.assistantMessageID),
@ -461,7 +460,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
match.time.completed = event.created
})
break
case "tool.failed":
case "session.tool.failed":
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestTool(
message.assistant(draft, index, event.data.assistantMessageID),
@ -484,7 +483,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
match.time.completed = event.created
})
break
case "reasoning.started":
case "session.reasoning.started":
message.update(event.data.sessionID, (draft, index) => {
message.assistant(draft, index, event.data.assistantMessageID)?.content.push({
type: "reasoning",
@ -495,7 +494,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
})
})
break
case "reasoning.delta":
case "session.reasoning.delta":
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestReasoning(
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
})
break
case "reasoning.ended":
case "session.reasoning.ended":
message.update(event.data.sessionID, (draft, index) => {
const match = message.latestReasoning(
message.assistant(draft, index, event.data.assistantMessageID),
@ -517,25 +516,25 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
}
})
break
case "retried":
case "compaction.started":
case "session.retried":
case "session.compaction.started":
setStore("session", "status", event.data.sessionID, "running")
break
case "execution.settled":
case "session.execution.settled":
setStore("session", "status", event.data.sessionID, "idle")
break
case "revert.staged":
case "session.revert.staged":
if (store.session.info[event.data.sessionID])
setStore("session", "info", event.data.sessionID, "revert", event.data.revert)
break
case "revert.cleared":
case "revert.committed":
case "session.revert.cleared":
case "session.revert.committed":
if (store.session.info[event.data.sessionID])
setStore("session", "info", event.data.sessionID, "revert", undefined)
break
case "compaction.delta":
case "session.compaction.delta":
break
case "compaction.ended":
case "session.compaction.ended":
message.update(event.data.sessionID, (draft, index) => {
message.append(draft, index, {
id: messageIDFromEvent(event.id),

View file

@ -74,17 +74,17 @@ const tui: TuiPlugin = async (api) => {
notify(api, sessionID, "Session done", session?.parentID ? "subagent_done" : "done")
}
api.event.on("prompt.promoted", (event) => started(event.data.sessionID))
api.event.on("shell.started", (event) => started(event.data.sessionID))
api.event.on("step.started", (event) => started(event.data.sessionID))
api.event.on("retried", (event) => started(event.data.sessionID))
api.event.on("compaction.started", (event) => started(event.data.sessionID))
api.event.on("shell.ended", (event) => ended(event.data.sessionID))
api.event.on("step.ended", (event) => {
api.event.on("session.prompt.promoted", (event) => started(event.data.sessionID))
api.event.on("session.shell.started", (event) => started(event.data.sessionID))
api.event.on("session.step.started", (event) => started(event.data.sessionID))
api.event.on("session.retried", (event) => started(event.data.sessionID))
api.event.on("session.compaction.started", (event) => started(event.data.sessionID))
api.event.on("session.shell.ended", (event) => ended(event.data.sessionID))
api.event.on("session.step.ended", (event) => {
if (event.data.finish === "tool-calls") return
ended(event.data.sessionID)
})
api.event.on("step.failed", (event) => {
api.event.on("session.step.failed", (event) => {
const sessionID = event.data.sessionID
if (!active.has(sessionID)) return
errored.add(sessionID)

View file

@ -1346,7 +1346,7 @@ function RevertMessage(props: {
function ShellMessage(props: { message: Extract<SessionMessage, { type: "shell" }> }) {
const { theme } = useTheme()
const output = createMemo(() => stripAnsi(props.message.output.trim()))
const output = createMemo(() => stripAnsi(props.message.output?.output.trim() ?? ""))
return (
<box
@ -1359,7 +1359,7 @@ function ShellMessage(props: { message: Extract<SessionMessage, { type: "shell"
customBorderChars={SplitBorder.customBorderChars}
borderColor={theme.background}
>
<text fg={theme.text}>$ {props.message.command}</text>
<text fg={theme.text}>$ {props.message.shell.command}</text>
<Show when={output()}>
<text fg={theme.textMuted}>{output()}</text>
</Show>
@ -1408,13 +1408,7 @@ function UserMessage(props: { message: SessionMessageUser }) {
>
<text fg={theme.text}>{props.message.text}</text>
<Show when={files().length}>
<box
flexDirection="row"
paddingBottom={metadataVisible() ? 1 : 0}
paddingTop={1}
gap={1}
flexWrap="wrap"
>
<box flexDirection="row" paddingBottom={metadataVisible() ? 1 : 0} paddingTop={1} gap={1} flexWrap="wrap">
<For each={files()}>
{(file) => {
const directory = file.mime === "application/x-directory"
@ -1744,7 +1738,8 @@ function ToolPart(props: { part: SessionMessageAssistantTool }) {
return Boolean(shellID && data.shell.get(shellID))
}
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 false
@ -2660,7 +2655,8 @@ function formatSessionTranscript(
) {
const body = messages.flatMap((message) => {
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 []
const content = message.content.flatMap((item) => {
if (item.type === "text") return [item.text]

View file

@ -133,42 +133,42 @@ export function createSessionRows(sessionID: Accessor<string>) {
if (event.data.sessionID === sessionID()) appendMessage(event.data.inputID)
}
const subscriptions = [
data.on("prompt.admitted", input),
data.on("prompt.promoted", input),
data.on("session.prompt.admitted", input),
data.on("session.prompt.promoted", input),
data.on("session.context.updated", message),
data.on("synthetic", (event) => {
data.on("session.synthetic", (event) => {
if (event.data.sessionID === sessionID() && event.data.description?.trim())
appendMessage(event.id.replace(/^evt_/, "msg_"))
}),
data.on("shell.started", message),
data.on("agent.selected", message),
data.on("model.selected", message),
data.on("compaction.ended", message),
data.on("text.delta", (event) => {
data.on("session.shell.started", message),
data.on("session.agent.selected", message),
data.on("session.model.selected", message),
data.on("session.compaction.ended", message),
data.on("session.text.delta", (event) => {
if (event.data.sessionID === sessionID())
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())
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())
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())
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())
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
appendFooter(event.data.assistantMessageID)
}),
data.on("step.failed", (event) => {
data.on("session.step.failed", (event) => {
if (event.data.sessionID === sessionID()) appendFooter(event.data.assistantMessageID)
}),
]

View file

@ -91,7 +91,7 @@ function stepStarted(id: string, sessionID = "session"): V2Event {
return {
id,
created: 0,
type: "step.started",
type: "session.step.started",
durable: durable(sessionID),
data: {
sessionID,
@ -106,7 +106,7 @@ function stepEnded(id: string, sessionID = "session", finish = "stop"): V2Event
return {
id,
created: 0,
type: "step.ended",
type: "session.step.ended",
durable: durable(sessionID),
data: {
sessionID,
@ -122,7 +122,7 @@ function stepFailed(id: string, sessionID = "session"): V2Event {
return {
id,
created: 0,
type: "step.failed",
type: "session.step.failed",
durable: durable(sessionID),
data: {
sessionID,

View file

@ -269,7 +269,7 @@ test("tracks session status from active sessions and execution events", async ()
emitEvent(events, {
id: "evt_step_started",
created: 0,
type: "step.started",
type: "session.step.started",
durable: durable("session-live"),
data: {
sessionID: "session-live",
@ -283,7 +283,7 @@ test("tracks session status from active sessions and execution events", async ()
emitEvent(events, {
id: "evt_step_ended",
created: 0,
type: "step.ended",
type: "session.step.ended",
durable: durable("session-live", 1, 2),
data: {
sessionID: "session-live",
@ -302,7 +302,7 @@ test("tracks session status from active sessions and execution events", async ()
emitEvent(events, {
id: "evt_execution_settled",
created: 0,
type: "execution.settled",
type: "session.execution.settled",
data: {
sessionID: "session-live",
outcome: "success",
@ -313,7 +313,7 @@ test("tracks session status from active sessions and execution events", async ()
emitEvent(events, {
id: "evt_failed_step_started",
created: 0,
type: "step.started",
type: "session.step.started",
durable: durable("session-failed"),
data: {
sessionID: "session-failed",
@ -327,7 +327,7 @@ test("tracks session status from active sessions and execution events", async ()
emitEvent(events, {
id: "evt_step_failed",
created: 0,
type: "step.failed",
type: "session.step.failed",
durable: durable("session-failed", 1, 2),
data: {
sessionID: "session-failed",
@ -344,7 +344,7 @@ test("tracks session status from active sessions and execution events", async ()
emitEvent(events, {
id: "evt_failed_execution_settled",
created: 0,
type: "execution.settled",
type: "session.execution.settled",
data: {
sessionID: "session-failed",
outcome: "failure",
@ -797,14 +797,14 @@ test("settles pending tools when a live failure arrives", async () => {
emitEvent(events, {
id: "evt_agent_1",
created: 0,
type: "agent.selected",
type: "session.agent.selected",
durable: durable("session-1"),
data: { sessionID: "session-1", agent: "build" },
})
emitEvent(events, {
id: "evt_model_1",
created: 0,
type: "model.selected",
type: "session.model.selected",
durable: durable("session-1", 1),
data: {
sessionID: "session-1",
@ -814,7 +814,7 @@ test("settles pending tools when a live failure arrives", async () => {
emitEvent(events, {
id: "evt_step_started_1",
created: 0,
type: "step.started",
type: "session.step.started",
durable: durable("session-1", 2),
data: {
sessionID: "session-1",
@ -826,7 +826,7 @@ test("settles pending tools when a live failure arrives", async () => {
emitEvent(events, {
id: "evt_input_1",
created: 0,
type: "tool.input.started",
type: "session.tool.input.started",
durable: durable("session-1", 3),
data: {
sessionID: "session-1",
@ -838,7 +838,7 @@ test("settles pending tools when a live failure arrives", async () => {
emitEvent(events, {
id: "evt_called_1",
created: 0,
type: "tool.called",
type: "session.tool.called",
durable: durable("session-1", 4),
data: {
sessionID: "session-1",
@ -852,7 +852,7 @@ test("settles pending tools when a live failure arrives", async () => {
emitEvent(events, {
id: "evt_failed_1",
created: 0,
type: "tool.failed",
type: "session.tool.failed",
durable: durable("session-1", 5),
data: {
sessionID: "session-1",
@ -942,7 +942,7 @@ test("renders admitted prompts immediately with queued marker and clears when pr
emitEvent(events, {
id: "evt_admitted_1",
created: 0,
type: "prompt.admitted",
type: "session.prompt.admitted",
durable: durable(sessionID),
data: {
sessionID,
@ -961,7 +961,7 @@ test("renders admitted prompts immediately with queued marker and clears when pr
emitEvent(events, {
id: "evt_prompted_1",
created: 0,
type: "prompt.promoted",
type: "session.prompt.promoted",
durable: durable(sessionID, 1),
data: {
sessionID,
@ -969,8 +969,8 @@ test("renders admitted prompts immediately with queued marker and clears when pr
},
})
await wait(() => received.at(-1) === "prompt.promoted")
expect(received.slice(-2)).toEqual(["prompt.admitted", "prompt.promoted"])
await wait(() => received.at(-1) === "session.prompt.promoted")
expect(received.slice(-2)).toEqual(["session.prompt.admitted", "session.prompt.promoted"])
unsubscribe()
const message = sync.session.message.list(sessionID)?.[0]
expect(message?.type).toBe("user")

View file

@ -9,7 +9,7 @@ export type ProviderIconProps = JSX.SVGElementTags["svg"] & {
export const ProviderIcon: Component<ProviderIconProps> = (props) => {
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 (
<svg
data-component="provider-icon"

View file

@ -908,7 +908,7 @@ Compatibility:
## 2026-07-03: Normalize Session Event Names And Envelope Time
- 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.
- 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.
@ -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.
- 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.
## 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.

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.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.