Просмотр исходного кода

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

Kit Langton 1 месяц назад
Родитель
Сommit
650d774372
49 измененных файлов с 1078 добавлено и 859 удалено
  1. 1 1
      packages/app/src/components/dialog-custom-provider.tsx
  2. 1 1
      packages/app/src/components/settings-providers.tsx
  3. 1 1
      packages/app/src/components/settings-v2/providers.tsx
  4. 198 76
      packages/client/src/promise/generated/types.ts
  5. 5 5
      packages/client/test/effect.test.ts
  6. 2 2
      packages/client/test/promise.test.ts
  7. 1 0
      packages/core/src/database/migration.gen.ts
  8. 14 0
      packages/core/src/database/migration/20260703190000_reset_v2_shell_event_payloads.ts
  9. 44 22
      packages/core/src/session.ts
  10. 1 1
      packages/core/src/session/compaction.ts
  11. 54 52
      packages/core/src/session/message-updater.ts
  12. 14 7
      packages/core/src/session/projector.ts
  13. 1 1
      packages/core/src/session/runner/to-llm-message.ts
  14. 10 8
      packages/core/test/session-create.test.ts
  15. 4 4
      packages/core/test/session-log.test.ts
  16. 25 6
      packages/core/test/session-projector.test.ts
  17. 5 5
      packages/core/test/session-prompt.test.ts
  18. 13 3
      packages/core/test/session-runner-message.test.ts
  19. 6 6
      packages/core/test/session-runner-recorded.test.ts
  20. 4 4
      packages/core/test/session-runner-tool-events.test.ts
  21. 3 3
      packages/core/test/session-runner.test.ts
  22. 15 15
      packages/opencode/src/cli/cmd/run/noninteractive.ts
  23. 15 15
      packages/opencode/src/cli/cmd/run/session-data.ts
  24. 15 15
      packages/opencode/src/cli/cmd/run/stream-v2.subagent.ts
  25. 77 48
      packages/opencode/src/cli/cmd/run/stream-v2.transport.ts
  26. 2 2
      packages/opencode/test/cli/run/noninteractive.test.ts
  27. 6 6
      packages/opencode/test/cli/run/session-data.test.ts
  28. 94 45
      packages/opencode/test/cli/run/stream-v2.transport.test.ts
  29. 2 2
      packages/opencode/test/server/httpapi-pty.test.ts
  30. 1 1
      packages/opencode/test/server/httpapi-sdk.test.ts
  31. 2 2
      packages/opencode/test/tool/apply_patch.test.ts
  32. 2 2
      packages/opencode/test/tool/fixtures/models-api.json
  33. 13 13
      packages/opencode/test/v2/session-message-updater.test.ts
  34. 38 38
      packages/schema/src/session-event.ts
  35. 3 3
      packages/schema/src/session-message.ts
  36. 32 32
      packages/schema/test/event-manifest.test.ts
  37. 6 6
      packages/sdk-next/test/embedded.test.ts
  38. 2 2
      packages/sdk/js/script/build.ts
  39. 233 307
      packages/sdk/js/src/v2/gen/types.gen.ts
  40. 3 3
      packages/sdk/openapi.json
  41. 38 39
      packages/tui/src/context/data.tsx
  42. 8 8
      packages/tui/src/feature-plugins/system/notifications.ts
  43. 7 11
      packages/tui/src/routes/session/index.tsx
  44. 14 14
      packages/tui/src/routes/session/rows.ts
  45. 3 3
      packages/tui/test/cli/cmd/tui/notifications.test.ts
  46. 16 16
      packages/tui/test/cli/tui/data.test.tsx
  47. 1 1
      packages/ui/src/components/provider-icon.tsx
  48. 22 1
      specs/v2/schema-changelog.md
  49. 1 1
      specs/v2/session.md

+ 1 - 1
packages/app/src/components/dialog-custom-provider.tsx

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

+ 1 - 1
packages/app/src/components/settings-providers.tsx

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

+ 1 - 1
packages/app/src/components/settings-v2/providers.tsx

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

+ 198 - 76
packages/client/src/promise/generated/types.ts

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

+ 5 - 5
packages/client/test/effect.test.ts

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

+ 2 - 2
packages/client/test/promise.test.ts

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

+ 1 - 0
packages/core/src/database/migration.gen.ts

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

+ 14 - 0
packages/core/src/database/migration/20260703190000_reset_v2_shell_event_payloads.ts

@@ -0,0 +1,14 @@
+import { Effect } from "effect"
+import type { DatabaseMigration } from "../migration"
+
+export default {
+  id: "20260703190000_reset_v2_shell_event_payloads",
+  up(tx) {
+    return Effect.gen(function* () {
+      yield* tx.run(`DELETE FROM \`session_input\`;`)
+      yield* tx.run(`DELETE FROM \`session_message\`;`)
+      yield* tx.run(`DELETE FROM \`event\`;`)
+      yield* tx.run(`DELETE FROM \`event_sequence\`;`)
+    })
+  },
+} satisfies DatabaseMigration.Migration

+ 44 - 22
packages/core/src/session.ts

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

+ 1 - 1
packages/core/src/session/compaction.ts

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

+ 54 - 52
packages/core/src/session/message-updater.ts

@@ -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,
     })
   })
 }

+ 14 - 7
packages/core/src/session/projector.ts

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

+ 1 - 1
packages/core/src/session/runner/to-llm-message.ts

@@ -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,
         }),
       ]

+ 10 - 8
packages/core/test/session-create.test.ts

@@ -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 } }])
     }),
   )
 

+ 4 - 4
packages/core/test/session-log.test.ts

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

+ 25 - 6
packages/core/test/session-projector.test.ts

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

+ 5 - 5
packages/core/test/session-prompt.test.ts

@@ -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"]])
     }),
   )
 

+ 13 - 3
packages/core/test/session-runner-message.test.ts

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

+ 6 - 6
packages/core/test/session-runner-recorded.test.ts

@@ -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",
       ])
     }),
   )

+ 4 - 4
packages/core/test/session-runner-tool-events.test.ts

@@ -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", () => {

+ 3 - 3
packages/core/test/session-runner.test.ts

@@ -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")
     }),
   )
 

+ 15 - 15
packages/opencode/src/cli/cmd/run/noninteractive.ts

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

+ 15 - 15
packages/opencode/src/cli/cmd/run/session-data.ts

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

+ 15 - 15
packages/opencode/src/cli/cmd/run/stream-v2.subagent.ts

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

+ 77 - 48
packages/opencode/src/cli/cmd/run/stream-v2.transport.ts

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

+ 2 - 2
packages/opencode/test/cli/run/noninteractive.test.ts

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

+ 6 - 6
packages/opencode/test/cli/run/session-data.test.ts

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

+ 94 - 45
packages/opencode/test/cli/run/stream-v2.transport.test.ts

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

+ 2 - 2
packages/opencode/test/server/httpapi-pty.test.ts

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

+ 1 - 1
packages/opencode/test/server/httpapi-sdk.test.ts

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

+ 2 - 2
packages/opencode/test/tool/apply_patch.test.ts

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

+ 2 - 2
packages/opencode/test/tool/fixtures/models-api.json

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

+ 13 - 13
packages/opencode/test/v2/session-message-updater.test.ts

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

+ 38 - 38
packages/schema/src/session-event.ts

@@ -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 },
   })

+ 3 - 3
packages/schema/src/session-message.ts

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

+ 32 - 32
packages/schema/test/event-manifest.test.ts

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

+ 6 - 6
packages/sdk-next/test/embedded.test.ts

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

+ 2 - 2
packages/sdk/js/script/build.ts

@@ -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,
   )
 ) {

Разница между файлами не показана из-за своего большого размера
+ 233 - 307
packages/sdk/js/src/v2/gen/types.gen.ts


+ 3 - 3
packages/sdk/openapi.json

@@ -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"],

+ 38 - 39
packages/tui/src/context/data.tsx

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

+ 8 - 8
packages/tui/src/feature-plugins/system/notifications.ts

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

+ 7 - 11
packages/tui/src/routes/session/index.tsx

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

+ 14 - 14
packages/tui/src/routes/session/rows.ts

@@ -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)
     }),
   ]

+ 3 - 3
packages/tui/test/cli/cmd/tui/notifications.test.ts

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

+ 16 - 16
packages/tui/test/cli/tui/data.test.tsx

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

+ 1 - 1
packages/ui/src/components/provider-icon.tsx

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

+ 22 - 1
specs/v2/schema-changelog.md

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

+ 1 - 1
specs/v2/session.md

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

Некоторые файлы не были показаны из-за большого количества измененных файлов