@@ -4,7 +4,6 @@ import { and, asc, eq, isNull, lte } from "drizzle-orm"
44import { DateTime , Effect , Schema } from "effect"
55import type { Database } from "../database/database"
66import type { EventV2 } from "../event"
7- import { EventSequenceTable } from "../event/sql"
87import { NonNegativeInt } from "../schema"
98import { V2Schema } from "../v2-schema"
109import { SessionEvent } from "./event"
@@ -65,7 +64,7 @@ export const admit = Effect.fn("SessionInput.admit")(function* (
6564 if ( existing !== undefined ) return existing
6665 const timestamp = yield * DateTime . now
6766 return yield * events
68- . publish ( SessionEvent . PromptLifecycle . Admitted , {
67+ . publish ( SessionEvent . PromptAdmitted , {
6968 messageID : input . id ,
7069 sessionID : input . sessionID ,
7170 timestamp,
@@ -93,19 +92,6 @@ export const admit = Effect.fn("SessionInput.admit")(function* (
9392 )
9493} )
9594
96- export const latestSeq = Effect . fn ( "SessionInput.latestSeq" ) ( function * (
97- db : DatabaseService ,
98- sessionID : SessionSchema . ID ,
99- ) {
100- const row = yield * db
101- . select ( { seq : EventSequenceTable . seq } )
102- . from ( EventSequenceTable )
103- . where ( eq ( EventSequenceTable . aggregate_id , sessionID ) )
104- . get ( )
105- . pipe ( Effect . orDie )
106- return row ?. seq ?? - 1
107- } )
108-
10995export const projectAdmitted = Effect . fn ( "SessionInput.projectAdmitted" ) ( function * (
11096 db : DatabaseService ,
11197 input : {
@@ -117,6 +103,13 @@ export const projectAdmitted = Effect.fn("SessionInput.projectAdmitted")(functio
117103 readonly timeCreated : DateTime . Utc
118104 } ,
119105) {
106+ const message = yield * db
107+ . select ( { id : SessionMessageTable . id } )
108+ . from ( SessionMessageTable )
109+ . where ( eq ( SessionMessageTable . id , input . id ) )
110+ . get ( )
111+ . pipe ( Effect . orDie )
112+ if ( message !== undefined ) return yield * Effect . die ( new LifecycleConflict ( { id : input . id } ) )
120113 const stored = yield * db
121114 . insert ( SessionInputTable )
122115 . values ( {
@@ -134,12 +127,13 @@ export const projectAdmitted = Effect.fn("SessionInput.projectAdmitted")(functio
134127 if ( ! stored ) return yield * Effect . die ( new LifecycleConflict ( { id : input . id } ) )
135128} )
136129
137- export const projectPromoted = Effect . fn ( "SessionInput.projectPromoted " ) ( function * (
130+ export const projectPrompted = Effect . fn ( "SessionInput.projectPrompted " ) ( function * (
138131 db : DatabaseService ,
139132 input : {
140133 readonly id : SessionMessage . ID
141134 readonly sessionID : SessionSchema . ID
142135 readonly prompt : Prompt
136+ readonly delivery : Delivery
143137 readonly timeCreated : DateTime . Utc
144138 readonly promotedSeq : number
145139 } ,
@@ -157,14 +151,32 @@ export const projectPromoted = Effect.fn("SessionInput.projectPromoted")(functio
157151 . returning ( )
158152 . get ( )
159153 . pipe ( Effect . orDie )
160- if ( ! updated ) return yield * Effect . die ( new LifecycleConflict ( { id : input . id } ) )
161- const stored = fromRow ( updated )
162- if (
163- ! matchesPrompt ( stored , input ) ||
164- DateTime . toEpochMillis ( stored . timeCreated ) !== DateTime . toEpochMillis ( input . timeCreated )
165- )
166- return yield * Effect . die ( new LifecycleConflict ( { id : input . id } ) )
167- return toMessage ( stored )
154+ if ( updated ) {
155+ const stored = fromRow ( updated )
156+ if ( ! matchesProjection ( stored , input ) ) return yield * Effect . die ( new LifecycleConflict ( { id : input . id } ) )
157+ return
158+ }
159+
160+ const stored = yield * find ( db , input . id )
161+ if ( stored ) {
162+ if ( ! matchesProjection ( stored , input ) || stored . promotedSeq !== input . promotedSeq )
163+ return yield * Effect . die ( new LifecycleConflict ( { id : input . id } ) )
164+ return
165+ }
166+
167+ yield * db
168+ . insert ( SessionInputTable )
169+ . values ( {
170+ id : input . id ,
171+ session_id : input . sessionID ,
172+ prompt : encodePrompt ( input . prompt ) ,
173+ delivery : input . delivery ,
174+ admitted_seq : input . promotedSeq ,
175+ promoted_seq : input . promotedSeq ,
176+ time_created : DateTime . toEpochMillis ( input . timeCreated ) ,
177+ } )
178+ . run ( )
179+ . pipe ( Effect . orDie )
168180} )
169181
170182export const hasPending = Effect . fn ( "SessionInput.hasPending" ) ( function * (
@@ -201,35 +213,17 @@ const matchesPrompt = (input: Admitted, expected: { readonly sessionID: SessionS
201213 input . sessionID === expected . sessionID &&
202214 JSON . stringify ( encodePrompt ( input . prompt ) ) === JSON . stringify ( encodePrompt ( expected . prompt ) )
203215
204- export const projectLegacyPrompted = Effect . fn ( "SessionInput.projectLegacyPrompted" ) ( function * (
205- db : DatabaseService ,
206- input : {
207- readonly id : SessionMessage . ID
216+ const matchesProjection = (
217+ input : Admitted ,
218+ expected : {
208219 readonly sessionID : SessionSchema . ID
209220 readonly prompt : Prompt
210221 readonly delivery : Delivery
211222 readonly timeCreated : DateTime . Utc
212- readonly promotedSeq : number
213223 } ,
214- ) {
215- const inserted = yield * db
216- . insert ( SessionInputTable )
217- . values ( {
218- id : input . id ,
219- session_id : input . sessionID ,
220- admitted_seq : input . promotedSeq ,
221- prompt : encodePrompt ( input . prompt ) ,
222- delivery : input . delivery ,
223- promoted_seq : input . promotedSeq ,
224- time_created : DateTime . toEpochMillis ( input . timeCreated ) ,
225- } )
226- . onConflictDoNothing ( )
227- . returning ( )
228- . get ( )
229- . pipe ( Effect . orDie )
230- if ( ! inserted ) return yield * Effect . die ( "Prompt projection conflicts with admitted input" )
231- return fromRow ( inserted )
232- } )
224+ ) =>
225+ equivalent ( input , expected ) &&
226+ DateTime . toEpochMillis ( input . timeCreated ) === DateTime . toEpochMillis ( expected . timeCreated )
233227
234228const publish = Effect . fn ( "SessionInput.publish" ) ( function * (
235229 db : DatabaseService ,
@@ -238,18 +232,19 @@ const publish = Effect.fn("SessionInput.publish")(function* (
238232 rows : ReadonlyArray < typeof SessionInputTable . $inferSelect > ,
239233) {
240234 for ( const row of rows ) {
235+ const id = SessionMessage . ID . make ( row . id )
241236 yield * events
242- . publish ( SessionEvent . PromptLifecycle . Promoted , {
237+ . publish ( SessionEvent . Prompted , {
243238 sessionID,
244- timestamp : yield * DateTime . now ,
245- messageID : SessionMessage . ID . make ( row . id ) ,
239+ timestamp : DateTime . makeUnsafe ( row . time_created ) ,
240+ messageID : id ,
246241 prompt : decodePrompt ( row . prompt ) ,
247- timeCreated : DateTime . makeUnsafe ( row . time_created ) ,
242+ delivery : row . delivery ,
248243 } )
249244 . pipe (
250245 Effect . catchDefect ( ( defect ) =>
251246 defect instanceof LifecycleConflict
252- ? find ( db , SessionMessage . ID . make ( row . id ) ) . pipe (
247+ ? find ( db , id ) . pipe (
253248 Effect . flatMap ( ( stored ) => ( stored ?. promotedSeq === undefined ? Effect . die ( defect ) : Effect . void ) ) ,
254249 )
255250 : Effect . die ( defect ) ,
@@ -303,13 +298,3 @@ export const promoteNextQueued = Effect.fn("SessionInput.promoteNextQueued")(fun
303298 . pipe ( Effect . orDie )
304299 return row === undefined ? false : yield * publish ( db , events , sessionID , [ row ] ) . pipe ( Effect . as ( true ) )
305300} )
306-
307- const toMessage = ( input : Admitted ) =>
308- new SessionMessage . User ( {
309- id : input . id ,
310- type : "user" ,
311- text : input . prompt . text ,
312- files : input . prompt . files ,
313- agents : input . prompt . agents ,
314- time : { created : input . timeCreated } ,
315- } )
0 commit comments