Parcourir la source

fix session event typechecks and shell cwd

Dax Raad il y a 3 mois
Parent
commit
459af775e8

+ 1 - 0
packages/opencode/src/session/projectors.ts

@@ -2,6 +2,7 @@ import { NotFoundError, eq, and } from "../storage"
 import { SyncEvent } from "@/sync"
 import * as Session from "./session"
 import { MessageV2 } from "./message-v2"
+import "../v2/session-event"
 import { SessionTable, MessageTable, PartTable } from "./session.sql"
 import { Log } from "../util"
 

+ 3 - 3
packages/opencode/src/session/prompt.ts

@@ -795,7 +795,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
             "-l",
             "-c",
             `
-              __oc_cwd=$PWD
+              __oc_cwd=$OPENCODE_CWD
               [[ -f ~/.zshenv ]] && source ~/.zshenv >/dev/null 2>&1 || true
               [[ -f "\${ZDOTDIR:-$HOME}/.zshrc" ]] && source "\${ZDOTDIR:-$HOME}/.zshrc" >/dev/null 2>&1 || true
               cd "$__oc_cwd"
@@ -808,7 +808,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
             "-l",
             "-c",
             `
-              __oc_cwd=$PWD
+              __oc_cwd=$OPENCODE_CWD
               shopt -s expand_aliases
               [[ -f ~/.bashrc ]] && source ~/.bashrc >/dev/null 2>&1 || true
               cd "$__oc_cwd"
@@ -833,7 +833,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
       const cmd = ChildProcess.make(sh, args, {
         cwd,
         extendEnv: true,
-        env: { ...shellEnv.env, TERM: "dumb" },
+        env: { ...shellEnv.env, OPENCODE_CWD: cwd, TERM: "dumb" },
         stdin: "ignore",
         forceKillAfter: "3 seconds",
       })

+ 1 - 0
packages/opencode/src/session/schema.ts

@@ -8,6 +8,7 @@ export const SessionID = Schema.String.annotate({ [ZodOverride]: Identifier.sche
   Schema.brand("SessionID"),
   withStatics((s) => ({
     descending: (id?: string) => s.make(Identifier.descending("session", id)),
+    empty: () => s.make("ses_empty"),
     zod: zod(s),
   })),
 )

+ 213 - 373
packages/opencode/src/v2/session-event.ts

@@ -1,127 +1,132 @@
 import { Identifier } from "@/id/id"
 import { withStatics } from "@/util/schema"
-import * as DateTime from "effect/DateTime"
 import { Schema } from "effect"
+import { SyncEvent } from "@/sync"
+import { SessionID } from "@/session/schema"
+import * as DateTime from "effect/DateTime"
 
-export namespace SessionEvent {
-  export const ID = Schema.String.pipe(
-    Schema.brand("Session.Event.ID"),
-    withStatics((s) => ({
-      create: () => s.make(Identifier.create("evt", "ascending")),
-    })),
-  )
-  export type ID = Schema.Schema.Type<typeof ID>
-  type Stamp = Schema.Schema.Type<typeof Schema.DateTimeUtc>
-  type BaseInput = {
-    id?: ID
-    metadata?: Record<string, unknown>
-    timestamp?: Stamp
-  }
+export const ID = Schema.String.pipe(
+  Schema.brand("Session.Event.ID"),
+  withStatics((s) => ({
+    create: () => s.make(Identifier.create("evt", "ascending")),
+  })),
+)
+export type ID = Schema.Schema.Type<typeof ID>
+type Stamp = Schema.Schema.Type<typeof Schema.DateTimeUtc>
+type BaseInput = {
+  id?: ID
+  sessionID: SessionID
+  metadata?: Record<string, unknown>
+  timestamp?: Stamp
+}
 
-  const Base = {
-    id: ID,
-    metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
-    timestamp: Schema.DateTimeUtc,
+function defineEvent<Self>(identifier: string) {
+  return <const Type extends string, Fields extends Schema.Struct.Fields>(input: {
+    type: Type
+    schema: Fields
+    version?: number
+  }) => {
+    const RawEvent = Schema.Class<Self>(identifier)({
+      id: ID,
+      sessionID: SessionID,
+      metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
+      timestamp: Schema.DateTimeUtc,
+      type: Schema.Literal(input.type),
+      ...input.schema,
+    })
+    const Event = RawEvent as Exclude<typeof RawEvent, string>
+
+    const Sync = SyncEvent.define({
+      type: input.type,
+      version: input.version ?? 1,
+      aggregate: "sessionID",
+      schema: Event,
+    })
+
+    return Object.assign(Event, {
+      Sync,
+      create(value: BaseInput & Record<string, unknown>) {
+        return new (Event as unknown as new (value: Record<string, unknown>) => Self)({
+          ...value,
+          id: value.id ?? ID.create(),
+          sessionID: value.sessionID,
+          timestamp: value.timestamp ?? DateTime.makeUnsafe(Date.now()),
+          type: input.type,
+        })
+      },
+    })
   }
+}
 
-  export class Source extends Schema.Class<Source>("Session.Event.Source")({
-    start: Schema.Number,
-    end: Schema.Number,
-    text: Schema.String,
-  }) {}
+export class Source extends Schema.Class<Source>("Session.Event.Source")({
+  start: Schema.Number,
+  end: Schema.Number,
+  text: Schema.String,
+}) {}
 
-  export class FileAttachment extends Schema.Class<FileAttachment>("Session.Event.FileAttachment")({
-    uri: Schema.String,
-    mime: Schema.String,
-    name: Schema.String.pipe(Schema.optional),
-    description: Schema.String.pipe(Schema.optional),
-    source: Source.pipe(Schema.optional),
-  }) {
-    static create(input: FileAttachment) {
-      return new FileAttachment({
-        uri: input.uri,
-        mime: input.mime,
-        name: input.name,
-        description: input.description,
-        source: input.source,
-      })
-    }
+export class FileAttachment extends Schema.Class<FileAttachment>("Session.Event.FileAttachment")({
+  uri: Schema.String,
+  mime: Schema.String,
+  name: Schema.String.pipe(Schema.optional),
+  description: Schema.String.pipe(Schema.optional),
+  source: Source.pipe(Schema.optional),
+}) {
+  static create(input: FileAttachment) {
+    return new FileAttachment({
+      uri: input.uri,
+      mime: input.mime,
+      name: input.name,
+      description: input.description,
+      source: input.source,
+    })
   }
+}
 
-  export class AgentAttachment extends Schema.Class<AgentAttachment>("Session.Event.AgentAttachment")({
-    name: Schema.String,
-    source: Source.pipe(Schema.optional),
-  }) {}
+export class AgentAttachment extends Schema.Class<AgentAttachment>("Session.Event.AgentAttachment")({
+  name: Schema.String,
+  source: Source.pipe(Schema.optional),
+}) {}
 
-  export class RetryError extends Schema.Class<RetryError>("Session.Event.Retry.Error")({
-    message: Schema.String,
-    statusCode: Schema.Number.pipe(Schema.optional),
-    isRetryable: Schema.Boolean,
-    responseHeaders: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
-    responseBody: Schema.String.pipe(Schema.optional),
-    metadata: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
-  }) {}
+export class RetryError extends Schema.Class<RetryError>("Session.Event.Retry.Error")({
+  message: Schema.String,
+  statusCode: Schema.Number.pipe(Schema.optional),
+  isRetryable: Schema.Boolean,
+  responseHeaders: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
+  responseBody: Schema.String.pipe(Schema.optional),
+  metadata: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
+}) {}
 
-  export class Prompt extends Schema.Class<Prompt>("Session.Event.Prompt")({
-    ...Base,
-    type: Schema.Literal("prompt"),
+export class Prompt extends defineEvent<Prompt>("Session.Event.Prompt")({
+  type: "prompt",
+  schema: {
     text: Schema.String,
     files: Schema.Array(FileAttachment).pipe(Schema.optional),
     agents: Schema.Array(AgentAttachment).pipe(Schema.optional),
-  }) {
-    static create(input: BaseInput & { text: string; files?: FileAttachment[]; agents?: AgentAttachment[] }) {
-      return new Prompt({
-        id: input.id ?? ID.create(),
-        type: "prompt",
-        timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
-        metadata: input.metadata,
-        text: input.text,
-        files: input.files,
-        agents: input.agents,
-      })
-    }
-  }
+  },
+}) {}
 
-  export class Synthetic extends Schema.Class<Synthetic>("Session.Event.Synthetic")({
-    ...Base,
-    type: Schema.Literal("synthetic"),
+export class Synthetic extends defineEvent<Synthetic>("Session.Event.Synthetic")({
+  type: "synthetic",
+  schema: {
     text: Schema.String,
-  }) {
-    static create(input: BaseInput & { text: string }) {
-      return new Synthetic({
-        id: input.id ?? ID.create(),
-        type: "synthetic",
-        timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
-        metadata: input.metadata,
-        text: input.text,
-      })
-    }
-  }
+  },
+}) {}
 
-  export namespace Step {
-    export class Started extends Schema.Class<Started>("Session.Event.Step.Started")({
-      ...Base,
-      type: Schema.Literal("step.started"),
+export namespace Step {
+  export class Started extends defineEvent<Started>("Session.Event.Step.Started")({
+    type: "step.started",
+    schema: {
       model: Schema.Struct({
         id: Schema.String,
         providerID: Schema.String,
         variant: Schema.String.pipe(Schema.optional),
       }),
-    }) {
-      static create(input: BaseInput & { model: { id: string; providerID: string; variant?: string } }) {
-        return new Started({
-          id: input.id ?? ID.create(),
-          type: "step.started",
-          timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
-          metadata: input.metadata,
-          model: input.model,
-        })
-      }
-    }
+    },
+  }) {}
 
-    export class Ended extends Schema.Class<Ended>("Session.Event.Step.Ended")({
-      ...Base,
-      type: Schema.Literal("step.ended"),
+  export class Ended extends defineEvent<Ended>("Session.Event.Step.Ended")({
+    type: "step.ended",
+    schema: {
       reason: Schema.String,
       cost: Schema.Number,
       tokens: Schema.Struct({
@@ -133,177 +138,82 @@ export namespace SessionEvent {
           write: Schema.Number,
         }),
       }),
-    }) {
-      static create(input: BaseInput & { reason: string; cost: number; tokens: Ended["tokens"] }) {
-        return new Ended({
-          id: input.id ?? ID.create(),
-          type: "step.ended",
-          timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
-          metadata: input.metadata,
-          reason: input.reason,
-          cost: input.cost,
-          tokens: input.tokens,
-        })
-      }
-    }
-  }
+    },
+  }) {}
+}
 
-  export namespace Text {
-    export class Started extends Schema.Class<Started>("Session.Event.Text.Started")({
-      ...Base,
-      type: Schema.Literal("text.started"),
-    }) {
-      static create(input: BaseInput = {}) {
-        return new Started({
-          id: input.id ?? ID.create(),
-          type: "text.started",
-          timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
-          metadata: input.metadata,
-        })
-      }
-    }
+export namespace Text {
+  export class Started extends defineEvent<Started>("Session.Event.Text.Started")({
+    type: "text.started",
+    schema: {},
+  }) {}
 
-    export class Delta extends Schema.Class<Delta>("Session.Event.Text.Delta")({
-      ...Base,
-      type: Schema.Literal("text.delta"),
+  export class Delta extends defineEvent<Delta>("Session.Event.Text.Delta")({
+    type: "text.delta",
+    schema: {
       delta: Schema.String,
-    }) {
-      static create(input: BaseInput & { delta: string }) {
-        return new Delta({
-          id: input.id ?? ID.create(),
-          type: "text.delta",
-          timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
-          metadata: input.metadata,
-          delta: input.delta,
-        })
-      }
-    }
+    },
+  }) {}
 
-    export class Ended extends Schema.Class<Ended>("Session.Event.Text.Ended")({
-      ...Base,
-      type: Schema.Literal("text.ended"),
+  export class Ended extends defineEvent<Ended>("Session.Event.Text.Ended")({
+    type: "text.ended",
+    schema: {
       text: Schema.String,
-    }) {
-      static create(input: BaseInput & { text: string }) {
-        return new Ended({
-          id: input.id ?? ID.create(),
-          type: "text.ended",
-          timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
-          metadata: input.metadata,
-          text: input.text,
-        })
-      }
-    }
-  }
+    },
+  }) {}
+}
 
-  export namespace Reasoning {
-    export class Started extends Schema.Class<Started>("Session.Event.Reasoning.Started")({
-      ...Base,
-      type: Schema.Literal("reasoning.started"),
-    }) {
-      static create(input: BaseInput = {}) {
-        return new Started({
-          id: input.id ?? ID.create(),
-          type: "reasoning.started",
-          timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
-          metadata: input.metadata,
-        })
-      }
-    }
+export namespace Reasoning {
+  export class Started extends defineEvent<Started>("Session.Event.Reasoning.Started")({
+    type: "reasoning.started",
+    schema: {},
+  }) {}
 
-    export class Delta extends Schema.Class<Delta>("Session.Event.Reasoning.Delta")({
-      ...Base,
-      type: Schema.Literal("reasoning.delta"),
+  export class Delta extends defineEvent<Delta>("Session.Event.Reasoning.Delta")({
+    type: "reasoning.delta",
+    schema: {
       delta: Schema.String,
-    }) {
-      static create(input: BaseInput & { delta: string }) {
-        return new Delta({
-          id: input.id ?? ID.create(),
-          type: "reasoning.delta",
-          timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
-          metadata: input.metadata,
-          delta: input.delta,
-        })
-      }
-    }
+    },
+  }) {}
 
-    export class Ended extends Schema.Class<Ended>("Session.Event.Reasoning.Ended")({
-      ...Base,
-      type: Schema.Literal("reasoning.ended"),
+  export class Ended extends defineEvent<Ended>("Session.Event.Reasoning.Ended")({
+    type: "reasoning.ended",
+    schema: {
       text: Schema.String,
-    }) {
-      static create(input: BaseInput & { text: string }) {
-        return new Ended({
-          id: input.id ?? ID.create(),
-          type: "reasoning.ended",
-          timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
-          metadata: input.metadata,
-          text: input.text,
-        })
-      }
-    }
-  }
+    },
+  }) {}
+}
 
-  export namespace Tool {
-    export namespace Input {
-      export class Started extends Schema.Class<Started>("Session.Event.Tool.Input.Started")({
-        ...Base,
+export namespace Tool {
+  export namespace Input {
+    export class Started extends defineEvent<Started>("Session.Event.Tool.Input.Started")({
+      type: "tool.input.started",
+      schema: {
         callID: Schema.String,
         name: Schema.String,
-        type: Schema.Literal("tool.input.started"),
-      }) {
-        static create(input: BaseInput & { callID: string; name: string }) {
-          return new Started({
-            id: input.id ?? ID.create(),
-            type: "tool.input.started",
-            timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
-            metadata: input.metadata,
-            callID: input.callID,
-            name: input.name,
-          })
-        }
-      }
+      },
+    }) {}
 
-      export class Delta extends Schema.Class<Delta>("Session.Event.Tool.Input.Delta")({
-        ...Base,
+    export class Delta extends defineEvent<Delta>("Session.Event.Tool.Input.Delta")({
+      type: "tool.input.delta",
+      schema: {
         callID: Schema.String,
-        type: Schema.Literal("tool.input.delta"),
         delta: Schema.String,
-      }) {
-        static create(input: BaseInput & { callID: string; delta: string }) {
-          return new Delta({
-            id: input.id ?? ID.create(),
-            type: "tool.input.delta",
-            timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
-            metadata: input.metadata,
-            callID: input.callID,
-            delta: input.delta,
-          })
-        }
-      }
+      },
+    }) {}
 
-      export class Ended extends Schema.Class<Ended>("Session.Event.Tool.Input.Ended")({
-        ...Base,
+    export class Ended extends defineEvent<Ended>("Session.Event.Tool.Input.Ended")({
+      type: "tool.input.ended",
+      schema: {
         callID: Schema.String,
-        type: Schema.Literal("tool.input.ended"),
         text: Schema.String,
-      }) {
-        static create(input: BaseInput & { callID: string; text: string }) {
-          return new Ended({
-            id: input.id ?? ID.create(),
-            type: "tool.input.ended",
-            timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
-            metadata: input.metadata,
-            callID: input.callID,
-            text: input.text,
-          })
-        }
-      }
-    }
+      },
+    }) {}
+  }
 
-    export class Called extends Schema.Class<Called>("Session.Event.Tool.Called")({
-      ...Base,
-      type: Schema.Literal("tool.called"),
+  export class Called extends defineEvent<Called>("Session.Event.Tool.Called")({
+    type: "tool.called",
+    schema: {
       callID: Schema.String,
       tool: Schema.String,
       input: Schema.Record(Schema.String, Schema.Unknown),
@@ -311,31 +221,12 @@ export namespace SessionEvent {
         executed: Schema.Boolean,
         metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
       }),
-    }) {
-      static create(
-        input: BaseInput & {
-          callID: string
-          tool: string
-          input: Record<string, unknown>
-          provider: Called["provider"]
-        },
-      ) {
-        return new Called({
-          id: input.id ?? ID.create(),
-          type: "tool.called",
-          timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
-          metadata: input.metadata,
-          callID: input.callID,
-          tool: input.tool,
-          input: input.input,
-          provider: input.provider,
-        })
-      }
-    }
+    },
+  }) {}
 
-    export class Success extends Schema.Class<Success>("Session.Event.Tool.Success")({
-      ...Base,
-      type: Schema.Literal("tool.success"),
+  export class Success extends defineEvent<Success>("Session.Event.Tool.Success")({
+    type: "tool.success",
+    schema: {
       callID: Schema.String,
       title: Schema.String,
       output: Schema.String.pipe(Schema.optional),
@@ -344,115 +235,64 @@ export namespace SessionEvent {
         executed: Schema.Boolean,
         metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
       }),
-    }) {
-      static create(
-        input: BaseInput & {
-          callID: string
-          title: string
-          output?: string
-          attachments?: FileAttachment[]
-          provider: Success["provider"]
-        },
-      ) {
-        return new Success({
-          id: input.id ?? ID.create(),
-          type: "tool.success",
-          timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
-          metadata: input.metadata,
-          callID: input.callID,
-          title: input.title,
-          output: input.output,
-          attachments: input.attachments,
-          provider: input.provider,
-        })
-      }
-    }
+    },
+  }) {}
 
-    export class Error extends Schema.Class<Error>("Session.Event.Tool.Error")({
-      ...Base,
-      type: Schema.Literal("tool.error"),
+  export class Error extends defineEvent<Error>("Session.Event.Tool.Error")({
+    type: "tool.error",
+    schema: {
       callID: Schema.String,
       error: Schema.String,
       provider: Schema.Struct({
         executed: Schema.Boolean,
         metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
       }),
-    }) {
-      static create(input: BaseInput & { callID: string; error: string; provider: Error["provider"] }) {
-        return new Error({
-          id: input.id ?? ID.create(),
-          type: "tool.error",
-          timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
-          metadata: input.metadata,
-          callID: input.callID,
-          error: input.error,
-          provider: input.provider,
-        })
-      }
-    }
-  }
+    },
+  }) {}
+}
 
-  export class Retried extends Schema.Class<Retried>("Session.Event.Retried")({
-    ...Base,
-    type: Schema.Literal("retried"),
+export class Retried extends defineEvent<Retried>("Session.Event.Retried")({
+  type: "retried",
+  schema: {
     attempt: Schema.Number,
     error: RetryError,
-  }) {
-    static create(input: BaseInput & { attempt: number; error: RetryError }) {
-      return new Retried({
-        id: input.id ?? ID.create(),
-        type: "retried",
-        timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
-        metadata: input.metadata,
-        attempt: input.attempt,
-        error: input.error,
-      })
-    }
-  }
+  },
+}) {}
 
-  export class Compacted extends Schema.Class<Compacted>("Session.Event.Compated")({
-    ...Base,
-    type: Schema.Literal("compacted"),
+export class Compacted extends defineEvent<Compacted>("Session.Event.Compacted")({
+  type: "compacted",
+  schema: {
     auto: Schema.Boolean,
     overflow: Schema.Boolean.pipe(Schema.optional),
-  }) {
-    static create(input: BaseInput & { auto: boolean; overflow?: boolean }) {
-      return new Compacted({
-        id: input.id ?? ID.create(),
-        type: "compacted",
-        timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
-        metadata: input.metadata,
-        auto: input.auto,
-        overflow: input.overflow,
-      })
-    }
-  }
+  },
+}) {}
 
-  export const Event = Schema.Union(
-    [
-      Prompt,
-      Synthetic,
-      Step.Started,
-      Step.Ended,
-      Text.Started,
-      Text.Delta,
-      Text.Ended,
-      Tool.Input.Started,
-      Tool.Input.Delta,
-      Tool.Input.Ended,
-      Tool.Called,
-      Tool.Success,
-      Tool.Error,
-      Reasoning.Started,
-      Reasoning.Delta,
-      Reasoning.Ended,
-      Retried,
-      Compacted,
-    ],
-    {
-      mode: "oneOf",
-    },
-  ).pipe(Schema.toTaggedUnion("type"))
-  export type Event = Schema.Schema.Type<typeof Event>
-  export type Type = Event["type"]
-}
+export const Event = Schema.Union(
+  [
+    Prompt,
+    Synthetic,
+    Step.Started,
+    Step.Ended,
+    Text.Started,
+    Text.Delta,
+    Text.Ended,
+    Tool.Input.Started,
+    Tool.Input.Delta,
+    Tool.Input.Ended,
+    Tool.Called,
+    Tool.Success,
+    Tool.Error,
+    Reasoning.Started,
+    Reasoning.Delta,
+    Reasoning.Ended,
+    Retried,
+    Compacted,
+  ],
+  {
+    mode: "oneOf",
+  },
+).pipe(Schema.toTaggedUnion("type"))
+export type Event = Schema.Schema.Type<typeof Event>
+export type Type = Event["type"]
+
+export * as SessionEvent from "./session-event"

+ 103 - 56
packages/opencode/test/session/session-entry-stepper.test.ts

@@ -4,8 +4,10 @@ import * as FastCheck from "effect/testing/FastCheck"
 import { SessionEntry } from "../../src/v2/session-entry"
 import { SessionEntryStepper } from "../../src/v2/session-entry-stepper"
 import { SessionEvent } from "../../src/v2/session-event"
+import { SessionID } from "../../src/session/schema"
 
 const time = (n: number) => DateTime.makeUnsafe(n)
+const sessionID = SessionID.empty()
 
 const word = FastCheck.string({ minLength: 1, maxLength: 8 })
 const text = FastCheck.string({ maxLength: 16 })
@@ -147,24 +149,34 @@ describe("session-entry-stepper", () => {
       const store = adapterStore()
       store.committed.push(assistant())
 
-      SessionEntryStepper.stepWith(adapterFor(store), SessionEvent.Prompt.create({ text: "hello", timestamp: time(1) }))
-      SessionEntryStepper.stepWith(adapterFor(store), SessionEvent.Reasoning.Started.create({ timestamp: time(2) }))
       SessionEntryStepper.stepWith(
         adapterFor(store),
-        SessionEvent.Reasoning.Delta.create({ delta: "thinking", timestamp: time(3) }),
+        SessionEvent.Prompt.create({ sessionID, text: "hello", timestamp: time(1) }),
       )
       SessionEntryStepper.stepWith(
         adapterFor(store),
-        SessionEvent.Reasoning.Ended.create({ text: "thought", timestamp: time(4) }),
+        SessionEvent.Reasoning.Started.create({ sessionID, timestamp: time(2) }),
       )
-      SessionEntryStepper.stepWith(adapterFor(store), SessionEvent.Text.Started.create({ timestamp: time(5) }))
       SessionEntryStepper.stepWith(
         adapterFor(store),
-        SessionEvent.Text.Delta.create({ delta: "world", timestamp: time(6) }),
+        SessionEvent.Reasoning.Delta.create({ sessionID, delta: "thinking", timestamp: time(3) }),
+      )
+      SessionEntryStepper.stepWith(
+        adapterFor(store),
+        SessionEvent.Reasoning.Ended.create({ sessionID, text: "thought", timestamp: time(4) }),
+      )
+      SessionEntryStepper.stepWith(
+        adapterFor(store),
+        SessionEvent.Text.Started.create({ sessionID, timestamp: time(5) }),
+      )
+      SessionEntryStepper.stepWith(
+        adapterFor(store),
+        SessionEvent.Text.Delta.create({ sessionID, delta: "world", timestamp: time(6) }),
       )
       SessionEntryStepper.stepWith(
         adapterFor(store),
         SessionEvent.Step.Ended.create({
+          sessionID,
           reason: "stop",
           cost: 1,
           tokens: {
@@ -199,15 +211,12 @@ describe("session-entry-stepper", () => {
 
       SessionEntryStepper.stepWith(
         adapterFor(store),
-        SessionEvent.Retried.create({
-          attempt: 1,
-          error: retryError("rate limited"),
-          timestamp: time(1),
-        }),
+        SessionEvent.Retried.create({ sessionID, attempt: 1, error: retryError("rate limited"), timestamp: time(1) }),
       )
       SessionEntryStepper.stepWith(
         adapterFor(store),
         SessionEvent.Retried.create({
+          sessionID,
           attempt: 2,
           error: retryError("provider overloaded"),
           timestamp: time(2),
@@ -253,9 +262,11 @@ describe("session-entry-stepper", () => {
       const state = memoryState()
       const adapter = SessionEntryStepper.memory(state)
       const committed = SessionEntry.User.fromEvent(
-        SessionEvent.Prompt.create({ text: "committed", timestamp: time(1) }),
+        SessionEvent.Prompt.create({ sessionID, text: "committed", timestamp: time(1) }),
+      )
+      const pending = SessionEntry.User.fromEvent(
+        SessionEvent.Prompt.create({ sessionID, text: "pending", timestamp: time(2) }),
       )
-      const pending = SessionEntry.User.fromEvent(SessionEvent.Prompt.create({ text: "pending", timestamp: time(2) }))
 
       adapter.appendEntry(committed)
       adapter.appendPending(pending)
@@ -269,15 +280,15 @@ describe("session-entry-stepper", () => {
 
       SessionEntryStepper.stepWith(
         SessionEntryStepper.memory(state),
-        SessionEvent.Reasoning.Started.create({ timestamp: time(1) }),
+        SessionEvent.Reasoning.Started.create({ sessionID, timestamp: time(1) }),
       )
       SessionEntryStepper.stepWith(
         SessionEntryStepper.memory(state),
-        SessionEvent.Reasoning.Delta.create({ delta: "draft", timestamp: time(2) }),
+        SessionEvent.Reasoning.Delta.create({ sessionID, delta: "draft", timestamp: time(2) }),
       )
       SessionEntryStepper.stepWith(
         SessionEntryStepper.memory(state),
-        SessionEvent.Reasoning.Ended.create({ text: "final", timestamp: time(3) }),
+        SessionEvent.Reasoning.Ended.create({ sessionID, text: "final", timestamp: time(3) }),
       )
 
       expect(reasons(state)).toEqual([{ type: "reasoning", text: "final" }])
@@ -288,11 +299,7 @@ describe("session-entry-stepper", () => {
 
       SessionEntryStepper.stepWith(
         SessionEntryStepper.memory(state),
-        SessionEvent.Retried.create({
-          attempt: 1,
-          error: retryError("rate limited"),
-          timestamp: time(1),
-        }),
+        SessionEvent.Retried.create({ sessionID, attempt: 1, error: retryError("rate limited"), timestamp: time(1) }),
       )
 
       expect(retriesOf(state)).toEqual([retry(1, "rate limited", 1)])
@@ -306,7 +313,7 @@ describe("session-entry-stepper", () => {
           FastCheck.property(word, (body) => {
             const next = SessionEntryStepper.step(
               memoryState(),
-              SessionEvent.Prompt.create({ text: body, timestamp: time(1) }),
+              SessionEvent.Prompt.create({ sessionID, text: body, timestamp: time(1) }),
             )
             expect(next.entries).toHaveLength(1)
             expect(next.entries[0]?.type).toBe("user")
@@ -322,7 +329,7 @@ describe("session-entry-stepper", () => {
           FastCheck.property(word, (body) => {
             const next = SessionEntryStepper.step(
               active(),
-              SessionEvent.Prompt.create({ text: body, timestamp: time(1) }),
+              SessionEvent.Prompt.create({ sessionID, text: body, timestamp: time(1) }),
             )
             expect(next.pending).toHaveLength(1)
             expect(next.pending[0]?.type).toBe("user")
@@ -340,9 +347,9 @@ describe("session-entry-stepper", () => {
               (state, part, i) =>
                 SessionEntryStepper.step(
                   state,
-                  SessionEvent.Text.Delta.create({ delta: part, timestamp: time(i + 2) }),
+                  SessionEvent.Text.Delta.create({ sessionID, delta: part, timestamp: time(i + 2) }),
                 ),
-              SessionEntryStepper.step(active(), SessionEvent.Text.Started.create({ timestamp: time(1) })),
+              SessionEntryStepper.step(active(), SessionEvent.Text.Started.create({ sessionID, timestamp: time(1) })),
             )
 
             expect(texts_of(next)).toEqual([
@@ -361,10 +368,12 @@ describe("session-entry-stepper", () => {
           FastCheck.property(texts, texts, (a, b) => {
             const next = run(
               [
-                SessionEvent.Text.Started.create({ timestamp: time(1) }),
-                ...a.map((x, i) => SessionEvent.Text.Delta.create({ delta: x, timestamp: time(i + 2) })),
-                SessionEvent.Text.Started.create({ timestamp: time(a.length + 2) }),
-                ...b.map((x, i) => SessionEvent.Text.Delta.create({ delta: x, timestamp: time(i + a.length + 3) })),
+                SessionEvent.Text.Started.create({ sessionID, timestamp: time(1) }),
+                ...a.map((x, i) => SessionEvent.Text.Delta.create({ sessionID, delta: x, timestamp: time(i + 2) })),
+                SessionEvent.Text.Started.create({ sessionID, timestamp: time(a.length + 2) }),
+                ...b.map((x, i) =>
+                  SessionEvent.Text.Delta.create({ sessionID, delta: x, timestamp: time(i + a.length + 3) }),
+                ),
               ],
               active(),
             )
@@ -383,9 +392,11 @@ describe("session-entry-stepper", () => {
           FastCheck.property(texts, text, (parts, end) => {
             const next = run(
               [
-                SessionEvent.Reasoning.Started.create({ timestamp: time(1) }),
-                ...parts.map((x, i) => SessionEvent.Reasoning.Delta.create({ delta: x, timestamp: time(i + 2) })),
-                SessionEvent.Reasoning.Ended.create({ text: end, timestamp: time(parts.length + 2) }),
+                SessionEvent.Reasoning.Started.create({ sessionID, timestamp: time(1) }),
+                ...parts.map((x, i) =>
+                  SessionEvent.Reasoning.Delta.create({ sessionID, delta: x, timestamp: time(i + 2) }),
+                ),
+                SessionEvent.Reasoning.Ended.create({ sessionID, text: end, timestamp: time(parts.length + 2) }),
               ],
               active(),
             )
@@ -414,11 +425,12 @@ describe("session-entry-stepper", () => {
             (callID, title, input, output, metadata, attachments, parts) => {
               const next = run(
                 [
-                  SessionEvent.Tool.Input.Started.create({ callID, name: "bash", timestamp: time(1) }),
+                  SessionEvent.Tool.Input.Started.create({ sessionID, callID, name: "bash", timestamp: time(1) }),
                   ...parts.map((x, i) =>
-                    SessionEvent.Tool.Input.Delta.create({ callID, delta: x, timestamp: time(i + 2) }),
+                    SessionEvent.Tool.Input.Delta.create({ sessionID, callID, delta: x, timestamp: time(i + 2) }),
                   ),
                   SessionEvent.Tool.Called.create({
+                    sessionID,
                     callID,
                     tool: "bash",
                     input,
@@ -426,6 +438,7 @@ describe("session-entry-stepper", () => {
                     timestamp: time(parts.length + 2),
                   }),
                   SessionEvent.Tool.Success.create({
+                    sessionID,
                     callID,
                     title,
                     output,
@@ -459,8 +472,9 @@ describe("session-entry-stepper", () => {
           FastCheck.property(word, dict, word, maybe(dict), (callID, input, error, metadata) => {
             const next = run(
               [
-                SessionEvent.Tool.Input.Started.create({ callID, name: "bash", timestamp: time(1) }),
+                SessionEvent.Tool.Input.Started.create({ sessionID, callID, name: "bash", timestamp: time(1) }),
                 SessionEvent.Tool.Called.create({
+                  sessionID,
                   callID,
                   tool: "bash",
                   input,
@@ -468,6 +482,7 @@ describe("session-entry-stepper", () => {
                   timestamp: time(2),
                 }),
                 SessionEvent.Tool.Error.create({
+                  sessionID,
                   callID,
                   error,
                   metadata,
@@ -496,8 +511,9 @@ describe("session-entry-stepper", () => {
           FastCheck.property(word, word, (callID, title) => {
             const next = run(
               [
-                SessionEvent.Tool.Input.Started.create({ callID, name: "bash", timestamp: time(1) }),
+                SessionEvent.Tool.Input.Started.create({ sessionID, callID, name: "bash", timestamp: time(1) }),
                 SessionEvent.Tool.Success.create({
+                  sessionID,
                   callID,
                   title,
                   provider: { executed: true },
@@ -520,6 +536,7 @@ describe("session-entry-stepper", () => {
         FastCheck.assert(
           FastCheck.property(FastCheck.integer({ min: 1, max: 1000 }), (n) => {
             const event = SessionEvent.Step.Ended.create({
+              sessionID,
               reason: "stop",
               cost: 1,
               tokens: {
@@ -552,7 +569,10 @@ describe("session-entry-stepper", () => {
         FastCheck.assert(
           FastCheck.property(word, (body) => {
             const old = memoryState()
-            const next = SessionEntryStepper.step(old, SessionEvent.Prompt.create({ text: body, timestamp: time(1) }))
+            const next = SessionEntryStepper.step(
+              old,
+              SessionEvent.Prompt.create({ sessionID, text: body, timestamp: time(1) }),
+            )
             expect(old).not.toBe(next)
             expect(old.entries).toHaveLength(0)
             expect(next.entries).toHaveLength(1)
@@ -565,7 +585,10 @@ describe("session-entry-stepper", () => {
         FastCheck.assert(
           FastCheck.property(word, (body) => {
             const old = active()
-            const next = SessionEntryStepper.step(old, SessionEvent.Prompt.create({ text: body, timestamp: time(1) }))
+            const next = SessionEntryStepper.step(
+              old,
+              SessionEvent.Prompt.create({ sessionID, text: body, timestamp: time(1) }),
+            )
             expect(old).not.toBe(next)
             expect(old.pending).toHaveLength(0)
             expect(next.pending).toHaveLength(1)
@@ -579,15 +602,17 @@ describe("session-entry-stepper", () => {
           FastCheck.property(texts, (parts) => {
             const next = run([
               SessionEvent.Step.Started.create({
+                sessionID,
                 model: {
                   id: "model",
                   providerID: "provider",
                 },
                 timestamp: time(1),
               }),
-              SessionEvent.Text.Started.create({ timestamp: time(2) }),
-              ...parts.map((x, i) => SessionEvent.Text.Delta.create({ delta: x, timestamp: time(i + 3) })),
+              SessionEvent.Text.Started.create({ sessionID, timestamp: time(2) }),
+              ...parts.map((x, i) => SessionEvent.Text.Delta.create({ sessionID, delta: x, timestamp: time(i + 3) })),
               SessionEvent.Step.Ended.create({
+                sessionID,
                 reason: "stop",
                 cost: 1,
                 tokens: {
@@ -623,17 +648,19 @@ describe("session-entry-stepper", () => {
         FastCheck.assert(
           FastCheck.property(word, texts, (body, parts) => {
             const next = run([
-              SessionEvent.Prompt.create({ text: body, timestamp: time(0) }),
+              SessionEvent.Prompt.create({ sessionID, text: body, timestamp: time(0) }),
               SessionEvent.Step.Started.create({
+                sessionID,
                 model: {
                   id: "model",
                   providerID: "provider",
                 },
                 timestamp: time(1),
               }),
-              SessionEvent.Text.Started.create({ timestamp: time(2) }),
-              ...parts.map((x, i) => SessionEvent.Text.Delta.create({ delta: x, timestamp: time(i + 3) })),
+              SessionEvent.Text.Started.create({ sessionID, timestamp: time(2) }),
+              ...parts.map((x, i) => SessionEvent.Text.Delta.create({ sessionID, delta: x, timestamp: time(i + 3) })),
               SessionEvent.Step.Ended.create({
+                sessionID,
                 reason: "stop",
                 cost: 1,
                 tokens: {
@@ -680,19 +707,28 @@ describe("session-entry-stepper", () => {
             (body, reason, end, input, title, output, metadata, attachments) => {
               const callID = "call"
               const next = run([
-                SessionEvent.Prompt.create({ text: body, timestamp: time(0) }),
+                SessionEvent.Prompt.create({ sessionID, text: body, timestamp: time(0) }),
                 SessionEvent.Step.Started.create({
+                  sessionID,
                   model: {
                     id: "model",
                     providerID: "provider",
                   },
                   timestamp: time(1),
                 }),
-                SessionEvent.Reasoning.Started.create({ timestamp: time(2) }),
-                ...reason.map((x, i) => SessionEvent.Reasoning.Delta.create({ delta: x, timestamp: time(i + 3) })),
-                SessionEvent.Reasoning.Ended.create({ text: end, timestamp: time(reason.length + 3) }),
-                SessionEvent.Tool.Input.Started.create({ callID, name: "bash", timestamp: time(reason.length + 4) }),
+                SessionEvent.Reasoning.Started.create({ sessionID, timestamp: time(2) }),
+                ...reason.map((x, i) =>
+                  SessionEvent.Reasoning.Delta.create({ sessionID, delta: x, timestamp: time(i + 3) }),
+                ),
+                SessionEvent.Reasoning.Ended.create({ sessionID, text: end, timestamp: time(reason.length + 3) }),
+                SessionEvent.Tool.Input.Started.create({
+                  sessionID,
+                  callID,
+                  name: "bash",
+                  timestamp: time(reason.length + 4),
+                }),
                 SessionEvent.Tool.Called.create({
+                  sessionID,
                   callID,
                   tool: "bash",
                   input,
@@ -700,6 +736,7 @@ describe("session-entry-stepper", () => {
                   timestamp: time(reason.length + 5),
                 }),
                 SessionEvent.Tool.Success.create({
+                  sessionID,
                   callID,
                   title,
                   output,
@@ -709,6 +746,7 @@ describe("session-entry-stepper", () => {
                   timestamp: time(reason.length + 6),
                 }),
                 SessionEvent.Step.Ended.create({
+                  sessionID,
                   reason: "stop",
                   cost: 1,
                   tokens: {
@@ -747,6 +785,7 @@ describe("session-entry-stepper", () => {
         const next = run(
           [
             SessionEvent.Step.Started.create({
+              sessionID,
               model: {
                 id: "model",
                 providerID: "provider",
@@ -771,8 +810,9 @@ describe("session-entry-stepper", () => {
           FastCheck.property(dict, dict, word, word, (a, b, title, error) => {
             const next = run(
               [
-                SessionEvent.Tool.Input.Started.create({ callID: "a", name: "bash", timestamp: time(1) }),
+                SessionEvent.Tool.Input.Started.create({ sessionID, callID: "a", name: "bash", timestamp: time(1) }),
                 SessionEvent.Tool.Called.create({
+                  sessionID,
                   callID: "a",
                   tool: "bash",
                   input: a,
@@ -780,14 +820,16 @@ describe("session-entry-stepper", () => {
                   timestamp: time(2),
                 }),
                 SessionEvent.Tool.Success.create({
+                  sessionID,
                   callID: "a",
                   title,
                   output: "done",
                   provider: { executed: true },
                   timestamp: time(3),
                 }),
-                SessionEvent.Tool.Input.Started.create({ callID: "b", name: "grep", timestamp: time(4) }),
+                SessionEvent.Tool.Input.Started.create({ sessionID, callID: "b", name: "grep", timestamp: time(4) }),
                 SessionEvent.Tool.Called.create({
+                  sessionID,
                   callID: "b",
                   tool: "bash",
                   input: b,
@@ -795,6 +837,7 @@ describe("session-entry-stepper", () => {
                   timestamp: time(5),
                 }),
                 SessionEvent.Tool.Error.create({
+                  sessionID,
                   callID: "b",
                   error,
                   provider: { executed: true },
@@ -827,11 +870,12 @@ describe("session-entry-stepper", () => {
           FastCheck.property(dict, dict, word, word, text, text, (a, b, titleA, titleB, deltaA, deltaB) => {
             const next = run(
               [
-                SessionEvent.Tool.Input.Started.create({ callID: "a", name: "bash", timestamp: time(1) }),
-                SessionEvent.Tool.Input.Started.create({ callID: "b", name: "grep", timestamp: time(2) }),
-                SessionEvent.Tool.Input.Delta.create({ callID: "a", delta: deltaA, timestamp: time(3) }),
-                SessionEvent.Tool.Input.Delta.create({ callID: "b", delta: deltaB, timestamp: time(4) }),
+                SessionEvent.Tool.Input.Started.create({ sessionID, callID: "a", name: "bash", timestamp: time(1) }),
+                SessionEvent.Tool.Input.Started.create({ sessionID, callID: "b", name: "grep", timestamp: time(2) }),
+                SessionEvent.Tool.Input.Delta.create({ sessionID, callID: "a", delta: deltaA, timestamp: time(3) }),
+                SessionEvent.Tool.Input.Delta.create({ sessionID, callID: "b", delta: deltaB, timestamp: time(4) }),
                 SessionEvent.Tool.Called.create({
+                  sessionID,
                   callID: "a",
                   tool: "bash",
                   input: a,
@@ -839,6 +883,7 @@ describe("session-entry-stepper", () => {
                   timestamp: time(5),
                 }),
                 SessionEvent.Tool.Called.create({
+                  sessionID,
                   callID: "b",
                   tool: "grep",
                   input: b,
@@ -846,6 +891,7 @@ describe("session-entry-stepper", () => {
                   timestamp: time(6),
                 }),
                 SessionEvent.Tool.Success.create({
+                  sessionID,
                   callID: "a",
                   title: titleA,
                   output: "done-a",
@@ -853,6 +899,7 @@ describe("session-entry-stepper", () => {
                   timestamp: time(7),
                 }),
                 SessionEvent.Tool.Success.create({
+                  sessionID,
                   callID: "b",
                   title: titleB,
                   output: "done-b",
@@ -884,7 +931,7 @@ describe("session-entry-stepper", () => {
           FastCheck.property(word, (body) => {
             const next = SessionEntryStepper.step(
               memoryState(),
-              SessionEvent.Synthetic.create({ text: body, timestamp: time(1) }),
+              SessionEvent.Synthetic.create({ sessionID, text: body, timestamp: time(1) }),
             )
             expect(next.entries).toHaveLength(1)
             expect(next.entries[0]?.type).toBe("synthetic")
@@ -900,7 +947,7 @@ describe("session-entry-stepper", () => {
           FastCheck.property(FastCheck.boolean(), maybe(FastCheck.boolean()), (auto, overflow) => {
             const next = SessionEntryStepper.step(
               memoryState(),
-              SessionEvent.Compacted.create({ auto, overflow, timestamp: time(1) }),
+              SessionEvent.Compacted.create({ sessionID, auto, overflow, timestamp: time(1) }),
             )
             expect(next.entries).toHaveLength(1)
             expect(next.entries[0]?.type).toBe("compaction")