diff --git a/packages/client/src/generated/types.ts b/packages/client/src/generated/types.ts index 3c2bd6e5c4..1bada8512c 100644 --- a/packages/client/src/generated/types.ts +++ b/packages/client/src/generated/types.ts @@ -799,12 +799,7 @@ export type SessionsHistoryOutput = { readonly timestamp: number readonly sessionID: string readonly parentID: string - readonly slug: string - readonly title: string - readonly agent?: string - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } readonly messageID?: string - readonly copiedSeq: number } } | { @@ -1283,12 +1278,7 @@ export type SessionsEventsOutput = readonly timestamp: number readonly sessionID: string readonly parentID: string - readonly slug: string - readonly title: string - readonly agent?: string - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } readonly messageID?: string - readonly copiedSeq: number } } | { diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 79da32aca7..4384180b68 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -298,35 +298,12 @@ export const layer = Layer.effect( : undefined if (input.messageID && !boundary) return yield* new MessageNotFoundError({ sessionID: input.sessionID, messageID: input.messageID }) - const copied = yield* db - .select({ seq: SessionMessageTable.seq }) - .from(SessionMessageTable) - .where( - and( - eq(SessionMessageTable.session_id, input.sessionID), - boundary === undefined ? undefined : lt(SessionMessageTable.seq, boundary.seq), - ), - ) - .orderBy(desc(SessionMessageTable.seq)) - .limit(1) - .get() - .pipe(Effect.orDie) const sessionID = SessionSchema.ID.create() yield* events.publish(SessionEvent.Forked, { sessionID, parentID: parent.id, - slug: Slug.create(), - title: forkTitle(parent.title), - agent: parent.agent, - model: parent.model, messageID: input.messageID, - copiedSeq: copied?.seq ?? 0, timestamp: yield* DateTime.now, - }, { - commit: (seq) => - copied && copied.seq > seq - ? EventV2.reserveSequence(db, sessionID, copied.seq) - : Effect.void, }) return yield* result.get(sessionID).pipe(Effect.orDie) }), @@ -563,12 +540,6 @@ export const defaultLayer = layer.pipe( Layer.orDie, ) -const forkTitle = (value: string) => { - const match = value.match(/^(.+) \(fork #(\d+)\)$/) - if (match) return `${match[1]} (fork #${Number.parseInt(match[2], 10) + 1})` - return `${value} (fork #1)` -} - const resolvePrompt = (input: PromptInput.Prompt) => Prompt.make({ text: input.text, diff --git a/packages/core/src/session/projector.ts b/packages/core/src/session/projector.ts index 47acd7d937..a9a069cf6a 100644 --- a/packages/core/src/session/projector.ts +++ b/packages/core/src/session/projector.ts @@ -15,6 +15,7 @@ import { WorkspaceV2 } from "../workspace" import { SessionContextEpoch } from "./context-epoch" import { MessageTable, PartTable, SessionInputTable, SessionMessageTable, SessionTable } from "./sql" import type { DeepMutable } from "../schema" +import { Slug } from "../util/slug" type DatabaseService = Database.Interface["db"] type MessageEvent = Exclude @@ -41,6 +42,12 @@ const emptyUsage = (): Usage => ({ tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, }) +const forkTitle = (value: string) => { + const match = value.match(/^(.+) \(fork #(\d+)\)$/) + if (match) return `${match[1]} (fork #${Number.parseInt(match[2], 10) + 1})` + return `${value} (fork #1)` +} + function usage(part: (typeof SessionV1.Event.PartUpdated.Type)["data"]["part"] | unknown): Usage | undefined { if (typeof part !== "object" || part === null) return undefined const value = part as Record @@ -144,6 +151,31 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* ( .get() .pipe(Effect.orDie) if (!parent) return yield* Effect.die(`Fork parent session not found: ${event.data.parentID}`) + const boundary = event.data.messageID + ? yield* db + .select({ seq: SessionMessageTable.seq }) + .from(SessionMessageTable) + .where( + and(eq(SessionMessageTable.session_id, event.data.parentID), eq(SessionMessageTable.id, event.data.messageID)), + ) + .get() + .pipe(Effect.orDie) + : undefined + if (event.data.messageID && !boundary) return yield* Effect.die(`Fork boundary message not found: ${event.data.messageID}`) + const copied = yield* db + .select({ seq: SessionMessageTable.seq }) + .from(SessionMessageTable) + .where( + and( + eq(SessionMessageTable.session_id, event.data.parentID), + boundary === undefined ? undefined : lt(SessionMessageTable.seq, boundary.seq), + ), + ) + .orderBy(desc(SessionMessageTable.seq)) + .limit(1) + .get() + .pipe(Effect.orDie) + const copiedSeq = copied?.seq ?? 0 const stored = yield* db .insert(SessionTable) @@ -152,12 +184,12 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* ( parent_id: event.data.parentID, project_id: parent.project_id, workspace_id: parent.workspace_id, - slug: event.data.slug, + slug: Slug.create(), directory: parent.directory, path: parent.path, - title: event.data.title, - agent: event.data.agent, - model: event.data.model, + title: forkTitle(parent.title), + agent: parent.agent, + model: parent.model, version: parent.version, cost: 0, tokens_input: 0, @@ -184,7 +216,7 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* ( and( eq(SessionMessageTable.session_id, event.data.parentID), gt(SessionMessageTable.seq, cursor), - event.data.messageID === undefined ? undefined : lt(SessionMessageTable.seq, event.data.copiedSeq + 1), + copiedSeq === 0 ? undefined : lt(SessionMessageTable.seq, copiedSeq + 1), ), ) .orderBy(asc(SessionMessageTable.seq)) @@ -273,7 +305,7 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* ( .where(eq(SessionTable.id, event.data.sessionID)) .run() .pipe(Effect.orDie) - if (event.data.copiedSeq > 0) yield* EventV2.reserveSequence(db, event.data.sessionID, event.data.copiedSeq) + if (copiedSeq > 0) yield* EventV2.reserveSequence(db, event.data.sessionID, copiedSeq) }) function run(db: DatabaseService, event: MessageEvent) { diff --git a/packages/core/test/session-create.test.ts b/packages/core/test/session-create.test.ts index df7e29db1e..16bdfd68b4 100644 --- a/packages/core/test/session-create.test.ts +++ b/packages/core/test/session-create.test.ts @@ -166,7 +166,7 @@ describe("SessionV2.create", () => { expect(history.events[0]).toMatchObject({ type: "session.next.forked", durable: { seq: 0 }, - data: { sessionID: forked.id, parentID: parent.id, copiedSeq: 3 }, + data: { sessionID: forked.id, parentID: parent.id }, }) expect(yield* SessionInput.find(db, forkContext[0]!.id)).toMatchObject({ sessionID: forked.id, @@ -216,7 +216,7 @@ describe("SessionV2.create", () => { const history = yield* session.history({ sessionID: forked.id, limit: 10 }) expect(context).toMatchObject([{ text: "First" }]) expect(context[0]?.id).not.toBe(first.id) - expect(history.events[0]).toMatchObject({ data: { copiedSeq: 2, messageID: second.id } }) + expect(history.events[0]).toMatchObject({ data: { messageID: second.id } }) }), ) diff --git a/packages/schema/src/session-event.ts b/packages/schema/src/session-event.ts index 0322af14c2..a66881a8ad 100644 --- a/packages/schema/src/session-event.ts +++ b/packages/schema/src/session-event.ts @@ -100,12 +100,7 @@ export const Forked = Event.define({ schema: { ...Base, parentID: SessionID, - slug: Schema.String, - title: Schema.String, - agent: Schema.String.pipe(optional), - model: Model.Ref.pipe(optional), messageID: SessionMessage.ID.pipe(optional), - copiedSeq: NonNegativeInt, }, }) export type Forked = typeof Forked.Type