@@ -348,6 +348,51 @@ export interface ProjectionTimelinePage {
348348 readonly hasMore : boolean ;
349349}
350350
351+ export type SideChatSource = Pick <
352+ OrchestrationV2ThreadProjection ,
353+ "thread" | "visibleTurnItems"
354+ > & {
355+ readonly omittedItems ?: number ;
356+ } ;
357+
358+ // Bound payload decoding before the byte-budget selector. Keep its priority
359+ // anchors even when they fall outside the recent window; older items remain
360+ // available through t3_thread_read. Metadata still follows normal fork visibility.
361+ const SIDE_CHAT_HISTORY_ITEMS = 256 ;
362+ function selectSideChatRows < T extends { readonly item : Pick < OrchestrationV2TurnItem , "type" > } > (
363+ rows : ReadonlyArray < T > ,
364+ ) {
365+ const eligible = rows . filter ( ( { item } ) =>
366+ [
367+ "user_message" ,
368+ "assistant_message" ,
369+ "command_execution" ,
370+ "error" ,
371+ "run_interrupt_result" ,
372+ "file_change" ,
373+ "proposed_plan" ,
374+ ] . includes ( item . type ) ,
375+ ) ;
376+ const selected = new Set < number > ( ) ;
377+ for ( const index of [
378+ eligible . findIndex ( ( { item } ) => item . type === "user_message" ) ,
379+ eligible . findLastIndex ( ( { item } ) => item . type === "user_message" ) ,
380+ eligible . findLastIndex ( ( { item } ) => item . type !== "user_message" ) ,
381+ ] )
382+ if ( index >= 0 ) selected . add ( index ) ;
383+ for (
384+ let index = eligible . length - 1 ;
385+ index >= 0 && selected . size < SIDE_CHAT_HISTORY_ITEMS ;
386+ index --
387+ ) {
388+ selected . add ( index ) ;
389+ }
390+ return {
391+ rows : eligible . filter ( ( _ , index ) => selected . has ( index ) ) ,
392+ omittedItems : eligible . length - selected . size ,
393+ } ;
394+ }
395+
351396export interface ShellSnapshotOptions {
352397 readonly location ?: "active" | "archive" ;
353398 readonly projectId ?: ProjectId ;
@@ -370,6 +415,9 @@ export interface ProjectionStoreV2Shape {
370415 throughEntryId ?: string ,
371416 conversationOnly ?: boolean ,
372417 ) => Effect . Effect < OrchestrationV2ThreadHistoryPage , ProjectionStoreV2Error > ;
418+ readonly getSideChatSource : (
419+ threadId : ThreadId ,
420+ ) => Effect . Effect < SideChatSource , ProjectionStoreV2Error > ;
373421 readonly getTimelinePage : (
374422 threadId : ThreadId ,
375423 options : ProjectionTimelinePageOptions ,
@@ -5160,6 +5208,48 @@ export const layer: Layer.Layer<ProjectionStoreV2, never, SqlClient.SqlClient> =
51605208 )
51615209 . pipe ( Effect . mapError ( controlReadError ( threadId ) ) ) ;
51625210
5211+ const getSideChatSource : ProjectionStoreV2Shape [ "getSideChatSource" ] = ( threadId ) =>
5212+ sql
5213+ . withTransaction (
5214+ Effect . gen ( function * ( ) {
5215+ const index = yield * readTimelineIndex ( threadId , new Set ( ) ) ;
5216+ const selected = selectSideChatRows (
5217+ index . visible . map ( ( row , position ) => ( { ...row , position } ) ) ,
5218+ ) ;
5219+ const sources = yield * encodeTimelineSources (
5220+ selected . rows . map ( ( row ) => ( {
5221+ threadId : row . sourceThreadId ,
5222+ id : row . sourceItemId ,
5223+ } ) ) ,
5224+ ) ;
5225+ const payloads = yield * sql < {
5226+ thread_id : string ;
5227+ turn_item_id : string ;
5228+ payload_json : string ;
5229+ } > `
5230+ SELECT item.thread_id, item.turn_item_id, item.payload_json
5231+ FROM orchestration_v2_projection_turn_items item JOIN json_each(${ sources } ) wanted
5232+ ON item.thread_id = json_extract(wanted.value, '$.threadId')
5233+ AND item.turn_item_id = json_extract(wanted.value, '$.id')` ;
5234+ const byIdentity = new Map (
5235+ payloads . map ( ( row ) => [ `${ row . thread_id } :${ row . turn_item_id } ` , row . payload_json ] ) ,
5236+ ) ;
5237+ const visibleTurnItems = yield * Effect . forEach ( selected . rows , ( row ) =>
5238+ Effect . gen ( function * ( ) {
5239+ const payload = byIdentity . get ( `${ row . sourceThreadId } :${ row . sourceItemId } ` ) ;
5240+ if ( payload === undefined ) return yield * new ProjectionStoreReadError ( { threadId } ) ;
5241+ return { ...row , item : yield * decodeTurnItemPayload ( payload ) } ;
5242+ } ) ,
5243+ ) ;
5244+ return {
5245+ thread : index . records . thread ,
5246+ visibleTurnItems,
5247+ omittedItems : selected . omittedItems ,
5248+ } ;
5249+ } ) ,
5250+ )
5251+ . pipe ( Effect . mapError ( controlReadError ( threadId ) ) ) ;
5252+
51635253 const getTimelinePage : ProjectionStoreV2Shape [ "getTimelinePage" ] = ( threadId , options ) =>
51645254 sql
51655255 . withTransaction (
@@ -6119,6 +6209,7 @@ export const layer: Layer.Layer<ProjectionStoreV2, never, SqlClient.SqlClient> =
61196209 getThreadSnapshot,
61206210 getThreadSnapshotWindow,
61216211 getTimelinePage,
6212+ getSideChatSource,
61226213 getThreadHistoryPage,
61236214 searchThread,
61246215 searchThreadStream,
@@ -6779,6 +6870,17 @@ export const layerMemory: Layer.Layer<ProjectionStoreV2> = Layer.effect(
67796870 } ) ,
67806871 ) ,
67816872 ) ,
6873+ getSideChatSource : ( threadId ) =>
6874+ service . getThreadProjection ( threadId ) . pipe (
6875+ Effect . map ( ( projection ) => {
6876+ const selected = selectSideChatRows ( projection . visibleTurnItems ) ;
6877+ return {
6878+ thread : projection . thread ,
6879+ visibleTurnItems : selected . rows ,
6880+ omittedItems : selected . omittedItems ,
6881+ } ;
6882+ } ) ,
6883+ ) ,
67826884 getTimelinePage : ( threadId , options ) =>
67836885 service . getThreadProjection ( threadId ) . pipe (
67846886 Effect . map ( ( projection ) => {
0 commit comments