@@ -5,7 +5,6 @@ import { NamedError } from "@opencode-ai/util/error"
55import { APICallError , convertToModelMessages , LoadAPIKeyError , type ModelMessage , type UIMessage } from "ai"
66import { LSP } from "../lsp"
77import { Snapshot } from "@/snapshot"
8- import { fn } from "@/util/fn"
98import { SyncEvent } from "../sync"
109import { Database , NotFoundError , and , desc , eq , inArray , lt , or } from "@/storage/db"
1110import { MessageTable , PartTable , SessionTable } from "./session.sql"
@@ -15,6 +14,7 @@ import { errorMessage } from "@/util/error"
1514import type { SystemError } from "bun"
1615import type { Provider } from "@/provider/provider"
1716import { ModelID , ProviderID } from "@/provider/schema"
17+ import { Effect } from "effect"
1818
1919/** Error shape thrown by Bun's fetch() when gzip/br decompression fails mid-stream */
2020interface FetchDecompressionError extends Error {
@@ -547,7 +547,7 @@ export namespace MessageV2 {
547547 and ( eq ( MessageTable . time_created , row . time ) , lt ( MessageTable . id , row . id ) ) ,
548548 )
549549
550- async function hydrate ( rows : ( typeof MessageTable . $inferSelect ) [ ] ) {
550+ function hydrate ( rows : ( typeof MessageTable . $inferSelect ) [ ] ) {
551551 const ids = rows . map ( ( row ) => row . id )
552552 const partByMessage = new Map < string , MessageV2 . Part [ ] > ( )
553553 if ( ids . length > 0 ) {
@@ -573,11 +573,11 @@ export namespace MessageV2 {
573573 } ) )
574574 }
575575
576- export async function toModelMessages (
576+ export const toModelMessagesEffect = Effect . fnUntraced ( function * (
577577 input : WithParts [ ] ,
578578 model : Provider . Model ,
579579 options ?: { stripMedia ?: boolean } ,
580- ) : Promise < ModelMessage [ ] > {
580+ ) {
581581 const result : UIMessage [ ] = [ ]
582582 const toolNames = new Set < string > ( )
583583 // Track media from tool results that need to be injected as user messages
@@ -800,74 +800,77 @@ export namespace MessageV2 {
800800
801801 const tools = Object . fromEntries ( Array . from ( toolNames ) . map ( ( toolName ) => [ toolName , { toModelOutput } ] ) )
802802
803- return await convertToModelMessages (
804- result . filter ( ( msg ) => msg . parts . some ( ( part ) => part . type !== "step-start" ) ) ,
805- {
806- //@ts -expect-error (convertToModelMessages expects a ToolSet but only actually needs tools[name]?.toModelOutput)
807- tools,
808- } ,
803+ return yield * Effect . promise ( ( ) =>
804+ convertToModelMessages (
805+ result . filter ( ( msg ) => msg . parts . some ( ( part ) => part . type !== "step-start" ) ) ,
806+ {
807+ //@ts -expect-error (convertToModelMessages expects a ToolSet but only actually needs tools[name]?.toModelOutput)
808+ tools,
809+ } ,
810+ ) ,
809811 )
812+ } )
813+
814+ export function toModelMessages (
815+ input : WithParts [ ] ,
816+ model : Provider . Model ,
817+ options ?: { stripMedia ?: boolean } ,
818+ ) : Promise < ModelMessage [ ] > {
819+ return Effect . runPromise ( toModelMessagesEffect ( input , model , options ) )
810820 }
811821
812- export const page = fn (
813- z . object ( {
814- sessionID : SessionID . zod ,
815- limit : z . number ( ) . int ( ) . positive ( ) ,
816- before : z . string ( ) . optional ( ) ,
817- } ) ,
818- async ( input ) => {
819- const before = input . before ? cursor . decode ( input . before ) : undefined
820- const where = before
821- ? and ( eq ( MessageTable . session_id , input . sessionID ) , older ( before ) )
822- : eq ( MessageTable . session_id , input . sessionID )
823- const rows = Database . use ( ( db ) =>
824- db
825- . select ( )
826- . from ( MessageTable )
827- . where ( where )
828- . orderBy ( desc ( MessageTable . time_created ) , desc ( MessageTable . id ) )
829- . limit ( input . limit + 1 )
830- . all ( ) ,
822+ export function page ( input : { sessionID : SessionID ; limit : number ; before ?: string } ) {
823+ const before = input . before ? cursor . decode ( input . before ) : undefined
824+ const where = before
825+ ? and ( eq ( MessageTable . session_id , input . sessionID ) , older ( before ) )
826+ : eq ( MessageTable . session_id , input . sessionID )
827+ const rows = Database . use ( ( db ) =>
828+ db
829+ . select ( )
830+ . from ( MessageTable )
831+ . where ( where )
832+ . orderBy ( desc ( MessageTable . time_created ) , desc ( MessageTable . id ) )
833+ . limit ( input . limit + 1 )
834+ . all ( ) ,
835+ )
836+ if ( rows . length === 0 ) {
837+ const row = Database . use ( ( db ) =>
838+ db . select ( { id : SessionTable . id } ) . from ( SessionTable ) . where ( eq ( SessionTable . id , input . sessionID ) ) . get ( ) ,
831839 )
832- if ( rows . length === 0 ) {
833- const row = Database . use ( ( db ) =>
834- db . select ( { id : SessionTable . id } ) . from ( SessionTable ) . where ( eq ( SessionTable . id , input . sessionID ) ) . get ( ) ,
835- )
836- if ( ! row ) throw new NotFoundError ( { message : `Session not found: ${ input . sessionID } ` } )
837- return {
838- items : [ ] as MessageV2 . WithParts [ ] ,
839- more : false ,
840- }
841- }
842-
843- const more = rows . length > input . limit
844- const page = more ? rows . slice ( 0 , input . limit ) : rows
845- const items = await hydrate ( page )
846- items . reverse ( )
847- const tail = page . at ( - 1 )
840+ if ( ! row ) throw new NotFoundError ( { message : `Session not found: ${ input . sessionID } ` } )
848841 return {
849- items,
850- more,
851- cursor : more && tail ? cursor . encode ( { id : tail . id , time : tail . time_created } ) : undefined ,
842+ items : [ ] as MessageV2 . WithParts [ ] ,
843+ more : false ,
852844 }
853- } ,
854- )
845+ }
846+
847+ const more = rows . length > input . limit
848+ const slice = more ? rows . slice ( 0 , input . limit ) : rows
849+ const items = hydrate ( slice )
850+ items . reverse ( )
851+ const tail = slice . at ( - 1 )
852+ return {
853+ items,
854+ more,
855+ cursor : more && tail ? cursor . encode ( { id : tail . id , time : tail . time_created } ) : undefined ,
856+ }
857+ }
855858
856- export const stream = fn ( SessionID . zod , async function * ( sessionID ) {
859+ export function * stream ( sessionID : SessionID ) {
857860 const size = 50
858861 let before : string | undefined
859862 while ( true ) {
860- const next = await page ( { sessionID, limit : size , before } )
863+ const next = page ( { sessionID, limit : size , before } )
861864 if ( next . items . length === 0 ) break
862865 for ( let i = next . items . length - 1 ; i >= 0 ; i -- ) {
863866 yield next . items [ i ]
864867 }
865868 if ( ! next . more || ! next . cursor ) break
866869 before = next . cursor
867870 }
868- } )
871+ }
869872
870- export const parts = fn ( MessageID . zod , async ( message_id ) => {
873+ export function parts ( message_id : MessageID ) {
871874 const rows = Database . use ( ( db ) =>
872875 db . select ( ) . from ( PartTable ) . where ( eq ( PartTable . message_id , message_id ) ) . orderBy ( PartTable . id ) . all ( ) ,
873876 )
@@ -880,33 +883,28 @@ export namespace MessageV2 {
880883 messageID : row . message_id ,
881884 } ) as MessageV2 . Part ,
882885 )
883- } )
886+ }
887+
888+ export function get ( input : { sessionID : SessionID ; messageID : MessageID } ) : WithParts {
889+ const row = Database . use ( ( db ) =>
890+ db
891+ . select ( )
892+ . from ( MessageTable )
893+ . where ( and ( eq ( MessageTable . id , input . messageID ) , eq ( MessageTable . session_id , input . sessionID ) ) )
894+ . get ( ) ,
895+ )
896+ if ( ! row ) throw new NotFoundError ( { message : `Message not found: ${ input . messageID } ` } )
897+ return {
898+ info : info ( row ) ,
899+ parts : parts ( input . messageID ) ,
900+ }
901+ }
884902
885- export const get = fn (
886- z . object ( {
887- sessionID : SessionID . zod ,
888- messageID : MessageID . zod ,
889- } ) ,
890- async ( input ) : Promise < WithParts > => {
891- const row = Database . use ( ( db ) =>
892- db
893- . select ( )
894- . from ( MessageTable )
895- . where ( and ( eq ( MessageTable . id , input . messageID ) , eq ( MessageTable . session_id , input . sessionID ) ) )
896- . get ( ) ,
897- )
898- if ( ! row ) throw new NotFoundError ( { message : `Message not found: ${ input . messageID } ` } )
899- return {
900- info : info ( row ) ,
901- parts : await parts ( input . messageID ) ,
902- }
903- } ,
904- )
905903
906- export async function filterCompacted ( stream : AsyncIterable < MessageV2 . WithParts > ) {
904+ export function filterCompacted ( msgs : Iterable < MessageV2 . WithParts > ) {
907905 const result = [ ] as MessageV2 . WithParts [ ]
908906 const completed = new Set < string > ( )
909- for await ( const msg of stream ) {
907+ for ( const msg of msgs ) {
910908 result . push ( msg )
911909 if (
912910 msg . info . role === "user" &&
@@ -921,6 +919,10 @@ export namespace MessageV2 {
921919 return result
922920 }
923921
922+ export const filterCompactedEffect = Effect . fnUntraced ( function * ( sessionID : SessionID ) {
923+ return filterCompacted ( stream ( sessionID ) )
924+ } )
925+
924926 export function fromError (
925927 e : unknown ,
926928 ctx : { providerID : ProviderID ; aborted ?: boolean } ,
0 commit comments