From beecb243bcd46cffdbc612b100430bd54e9a4ad4 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 4 Jul 2026 20:13:43 -0400 Subject: [PATCH] fix(core): harden compaction admission --- .../client/src/promise/generated/types.ts | 2 -- packages/client/test/effect.test.ts | 1 - packages/client/test/promise.test.ts | 1 - packages/core/src/session/input.ts | 24 ++++++++++++++----- packages/schema/src/session-input.ts | 9 +++++-- packages/sdk/js/src/v2/gen/types.gen.ts | 2 -- 6 files changed, 25 insertions(+), 14 deletions(-) diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 7804428e35..7fff47d41a 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -600,7 +600,6 @@ export type SessionPromptInput = { export type SessionPromptOutput = { readonly data: { - readonly type: "prompt" readonly admittedSeq: number readonly id: string readonly sessionID: string @@ -801,7 +800,6 @@ export type SessionCommandInput = { export type SessionCommandOutput = { readonly data: { - readonly type: "prompt" readonly admittedSeq: number readonly id: string readonly sessionID: string diff --git a/packages/client/test/effect.test.ts b/packages/client/test/effect.test.ts index 40765d7be8..17c5ca3221 100644 --- a/packages/client/test/effect.test.ts +++ b/packages/client/test/effect.test.ts @@ -212,7 +212,6 @@ const session = { const admission = { data: { - type: "prompt", admittedSeq: 0, id: "msg_test", sessionID: "ses_test", diff --git a/packages/client/test/promise.test.ts b/packages/client/test/promise.test.ts index ca2d5bba1b..d28174c536 100644 --- a/packages/client/test/promise.test.ts +++ b/packages/client/test/promise.test.ts @@ -309,7 +309,6 @@ const session = { const admission = { data: { - type: "prompt", admittedSeq: 0, id: "msg_test", sessionID: "ses_test", diff --git a/packages/core/src/session/input.ts b/packages/core/src/session/input.ts index c5d6cb7cc3..65e3019b78 100644 --- a/packages/core/src/session/input.ts +++ b/packages/core/src/session/input.ts @@ -2,7 +2,7 @@ export * as SessionInput from "./input" import { and, asc, eq, isNull } from "drizzle-orm" import { DateTime, Effect, Schema } from "effect" -import { Admitted, Compaction, Delivery, Entry } from "@opencode-ai/schema/session-input" +import { Admitted, Compaction, Delivery, Entry, PromptEntry } from "@opencode-ai/schema/session-input" import type { Database } from "../database/database" import type { EventV2 } from "../event" import { KeyedMutex } from "../effect/keyed-mutex" @@ -14,7 +14,7 @@ import { SessionInputTable, SessionMessageTable } from "./sql" type DatabaseService = Database.Interface["db"] -export { Admitted, Compaction, Delivery, Entry } +export { Admitted, Compaction, Delivery, Entry, PromptEntry } const decodePrompt = Schema.decodeUnknownSync(Prompt) const encodePrompt = Schema.encodeSync(Prompt) @@ -38,7 +38,7 @@ const fromRow = (row: typeof SessionInputTable.$inferSelect): Entry => { ...(row.promoted_seq === null ? {} : { handledSeq: row.promoted_seq }), }) if (!row.prompt || !row.delivery) throw new LifecycleConflict({ id: base.id }) - return Admitted.make({ + return PromptEntry.make({ ...base, type: "prompt", prompt: decodePrompt(row.prompt), @@ -47,6 +47,17 @@ const fromRow = (row: typeof SessionInputTable.$inferSelect): Entry => { }) } +const toAdmitted = (entry: PromptEntry): Admitted => + Admitted.make({ + admittedSeq: entry.admittedSeq, + id: entry.id, + sessionID: entry.sessionID, + prompt: entry.prompt, + delivery: entry.delivery, + timeCreated: entry.timeCreated, + ...(entry.promotedSeq === undefined ? {} : { promotedSeq: entry.promotedSeq }), + }) + export const find = Effect.fn("SessionInput.find")(function* (db: DatabaseService, id: SessionMessage.ID) { const row = yield* db.select().from(SessionInputTable).where(eq(SessionInputTable.id, id)).get().pipe(Effect.orDie) return row === undefined ? undefined : fromRow(row) @@ -88,7 +99,7 @@ export const admit = Effect.fn("SessionInput.admit")(function* ( const existing = yield* find(db, input.id) if (existing !== undefined) { if (existing.type !== "prompt") return yield* Effect.die(new LifecycleConflict({ id: input.id })) - return existing + return toAdmitted(existing) } return yield* events .publish(SessionEvent.PromptAdmitted, { @@ -103,7 +114,6 @@ export const admit = Effect.fn("SessionInput.admit")(function* ( ? Effect.die(new Error("Prompt admission event is missing aggregate sequence")) : Effect.succeed( Admitted.make({ - type: "prompt", admittedSeq: event.durable.seq, id: input.id, sessionID: input.sessionID, @@ -115,7 +125,9 @@ export const admit = Effect.fn("SessionInput.admit")(function* ( ), Effect.catchDefect((defect) => find(db, input.id).pipe( - Effect.flatMap((stored) => (stored?.type === "prompt" ? Effect.succeed(stored) : Effect.die(defect))), + Effect.flatMap((stored) => + stored?.type === "prompt" ? Effect.succeed(toAdmitted(stored)) : Effect.die(defect), + ), ), ), ) diff --git a/packages/schema/src/session-input.ts b/packages/schema/src/session-input.ts index a3e691dcd1..a59c00b40e 100644 --- a/packages/schema/src/session-input.ts +++ b/packages/schema/src/session-input.ts @@ -13,7 +13,6 @@ export type Delivery = SessionDelivery.Delivery export interface Admitted extends Schema.Schema.Type {} export const Admitted = Schema.Struct({ - type: Schema.Literal("prompt"), admittedSeq: NonNegativeInt, id: SessionMessage.ID, sessionID: SessionID, @@ -23,6 +22,12 @@ export const Admitted = Schema.Struct({ promotedSeq: NonNegativeInt.pipe(optional), }).annotate({ identifier: "SessionInput.Admitted" }) +export interface PromptEntry extends Schema.Schema.Type {} +export const PromptEntry = Schema.Struct({ + type: Schema.Literal("prompt"), + ...Admitted.fields, +}).annotate({ identifier: "SessionInput.PromptEntry" }) + export interface Compaction extends Schema.Schema.Type {} export const Compaction = Schema.Struct({ type: Schema.Literal("compaction"), @@ -33,5 +38,5 @@ export const Compaction = Schema.Struct({ handledSeq: NonNegativeInt.pipe(optional), }).annotate({ identifier: "SessionInput.Compaction" }) -export const Entry = Schema.Union([Admitted, Compaction]).pipe(Schema.toTaggedUnion("type")) +export const Entry = Schema.Union([PromptEntry, Compaction]).pipe(Schema.toTaggedUnion("type")) export type Entry = typeof Entry.Type diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 2cfd67fe39..9308e30282 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -4399,7 +4399,6 @@ export type PromptInputFileAttachment = { } export type SessionInputAdmitted = { - type: "prompt" admittedSeq: number id: string sessionID: string @@ -8697,7 +8696,6 @@ export type SessionV2InfoV2 = { } export type SessionInputAdmittedV2 = { - type: "prompt" admittedSeq: number id: string sessionID: string