@@ -55,6 +55,8 @@ const TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY = 10_000;
5555const TURN_MESSAGE_IDS_BY_TURN_TTL = Duration . minutes ( 120 ) ;
5656const STALE_REPLAY_ITEM_IDS_BY_TURN_CACHE_CAPACITY = 10_000 ;
5757const STALE_REPLAY_ITEM_IDS_BY_TURN_TTL = Duration . minutes ( 120 ) ;
58+ const CURSOR_REPLAY_REMAINING_ASSISTANT_TEXT_BY_THREAD_CACHE_CAPACITY = 10_000 ;
59+ const CURSOR_REPLAY_REMAINING_ASSISTANT_TEXT_BY_THREAD_TTL = Duration . minutes ( 120 ) ;
5860const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_CACHE_CAPACITY = 20_000 ;
5961const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_TTL = Duration . minutes ( 120 ) ;
6062const BUFFERED_PROPOSED_PLAN_BY_ID_CACHE_CAPACITY = 10_000 ;
@@ -228,6 +230,26 @@ function hasRenderableAssistantText(text: string | undefined): boolean {
228230 return ( text ?. trim ( ) . length ?? 0 ) > 0 ;
229231}
230232
233+ function joinedAssistantMessageText ( messages : ReadonlyArray < OrchestrationMessage > ) : string {
234+ let text = "" ;
235+ for ( let index = 0 ; index < messages . length ; index += 1 ) {
236+ const message = messages [ index ] ;
237+ if ( message ?. role === "assistant" && message . text . length > 0 ) {
238+ text += message . text ;
239+ }
240+ }
241+ return text ;
242+ }
243+
244+ function isCursorTurnlessAssistantDelta ( event : ProviderRuntimeEvent ) : boolean {
245+ return (
246+ String ( event . provider ) === "cursor" &&
247+ event . type === "content.delta" &&
248+ event . turnId === undefined &&
249+ event . payload . streamKind === "assistant_text"
250+ ) ;
251+ }
252+
231253function proposedPlanIdForTurn ( threadId : ThreadId , turnId : TurnId ) : string {
232254 return `plan:${ threadId } :turn:${ turnId } ` ;
233255}
@@ -797,6 +819,12 @@ const make = Effect.gen(function* () {
797819 lookup : ( ) => Effect . succeed ( new Set < string > ( ) ) ,
798820 } ) ;
799821
822+ const cursorReplayRemainingAssistantTextByThreadId = yield * Cache . make < ThreadId , string > ( {
823+ capacity : CURSOR_REPLAY_REMAINING_ASSISTANT_TEXT_BY_THREAD_CACHE_CAPACITY ,
824+ timeToLive : CURSOR_REPLAY_REMAINING_ASSISTANT_TEXT_BY_THREAD_TTL ,
825+ lookup : ( ) => Effect . succeed ( "" ) ,
826+ } ) ;
827+
800828 const bufferedProposedPlanById = yield * Cache . make < string , { text : string ; createdAt : string } > ( {
801829 capacity : BUFFERED_PROPOSED_PLAN_BY_ID_CACHE_CAPACITY ,
802830 timeToLive : BUFFERED_PROPOSED_PLAN_BY_ID_TTL ,
@@ -909,6 +937,39 @@ const make = Effect.gen(function* () {
909937 const clearStaleReplayItemsForTurn = ( threadId : ThreadId , turnId : TurnId ) =>
910938 Cache . invalidate ( staleReplayItemIdsByTurnKey , providerTurnKey ( threadId , turnId ) ) ;
911939
940+ const reconcileCursorTurnlessAssistantDelta = ( input : {
941+ threadId : ThreadId ;
942+ messages : ReadonlyArray < OrchestrationMessage > ;
943+ delta : string ;
944+ } ) =>
945+ Cache . getOption ( cursorReplayRemainingAssistantTextByThreadId , input . threadId ) . pipe (
946+ Effect . flatMap ( ( cachedRemaining ) =>
947+ Effect . gen ( function * ( ) {
948+ const remainingAlreadySeenText = Option . getOrElse ( cachedRemaining , ( ) =>
949+ joinedAssistantMessageText ( input . messages ) ,
950+ ) ;
951+ if ( remainingAlreadySeenText . length === 0 ) {
952+ yield * Cache . set ( cursorReplayRemainingAssistantTextByThreadId , input . threadId , "" ) ;
953+ return input . delta ;
954+ }
955+ if ( remainingAlreadySeenText . startsWith ( input . delta ) ) {
956+ yield * Cache . set (
957+ cursorReplayRemainingAssistantTextByThreadId ,
958+ input . threadId ,
959+ remainingAlreadySeenText . slice ( input . delta . length ) ,
960+ ) ;
961+ return "" ;
962+ }
963+ if ( input . delta . startsWith ( remainingAlreadySeenText ) ) {
964+ yield * Cache . set ( cursorReplayRemainingAssistantTextByThreadId , input . threadId , "" ) ;
965+ return input . delta . slice ( remainingAlreadySeenText . length ) ;
966+ }
967+ yield * Cache . set ( cursorReplayRemainingAssistantTextByThreadId , input . threadId , "" ) ;
968+ return input . delta ;
969+ } ) ,
970+ ) ,
971+ ) ;
972+
912973 const getActiveAssistantMessageIdForTurn = ( threadId : ThreadId , turnId : TurnId ) =>
913974 getAssistantSegmentStateForTurn ( threadId , turnId ) . pipe (
914975 Effect . map ( ( state ) =>
@@ -1565,6 +1626,16 @@ const make = Effect.gen(function* () {
15651626
15661627 if ( assistantDelta && assistantDelta . length > 0 ) {
15671628 const turnId = toTurnId ( event . turnId ) ;
1629+ const reconciledAssistantDelta = isCursorTurnlessAssistantDelta ( event )
1630+ ? yield * reconcileCursorTurnlessAssistantDelta ( {
1631+ threadId : thread . id ,
1632+ messages : ( yield * getLoadedThreadDetail ( ) ) ?. messages ?? [ ] ,
1633+ delta : assistantDelta ,
1634+ } )
1635+ : assistantDelta ;
1636+ if ( reconciledAssistantDelta . length === 0 ) {
1637+ return ;
1638+ }
15681639 const assistantMessageId = yield * getOrCreateAssistantMessageId ( {
15691640 threadId : thread . id ,
15701641 event,
@@ -1579,7 +1650,10 @@ const make = Effect.gen(function* () {
15791650 ( settings ) => ( settings . enableAssistantStreaming ? "streaming" : "buffered" ) ,
15801651 ) ;
15811652 if ( assistantDeliveryMode === "buffered" ) {
1582- const spillChunk = yield * appendBufferedAssistantText ( assistantMessageId , assistantDelta ) ;
1653+ const spillChunk = yield * appendBufferedAssistantText (
1654+ assistantMessageId ,
1655+ reconciledAssistantDelta ,
1656+ ) ;
15831657 if ( spillChunk . length > 0 ) {
15841658 yield * orchestrationEngine . dispatch ( {
15851659 type : "thread.message.assistant.delta" ,
@@ -1597,7 +1671,7 @@ const make = Effect.gen(function* () {
15971671 commandId : yield * providerCommandId ( event , "assistant-delta" ) ,
15981672 threadId : thread . id ,
15991673 messageId : assistantMessageId ,
1600- delta : assistantDelta ,
1674+ delta : reconciledAssistantDelta ,
16011675 ...( turnId ? { turnId } : { } ) ,
16021676 createdAt : now ,
16031677 } ) ;
@@ -1760,6 +1834,7 @@ const make = Effect.gen(function* () {
17601834 yield * clearAssistantMessageIdsForTurn ( thread . id , turnId ) ;
17611835 yield * clearAssistantSegmentStateForTurn ( thread . id , turnId ) ;
17621836 yield * clearStaleReplayItemsForTurn ( thread . id , turnId ) ;
1837+ yield * Cache . invalidate ( cursorReplayRemainingAssistantTextByThreadId , thread . id ) ;
17631838
17641839 yield * finalizeBufferedProposedPlan ( {
17651840 event,
@@ -1774,6 +1849,7 @@ const make = Effect.gen(function* () {
17741849
17751850 if ( event . type === "session.exited" ) {
17761851 yield * clearTurnStateForSession ( thread . id ) ;
1852+ yield * Cache . invalidate ( cursorReplayRemainingAssistantTextByThreadId , thread . id ) ;
17771853 }
17781854
17791855 if ( event . type === "runtime.error" ) {
0 commit comments