@@ -579,19 +579,26 @@ function normalizeThreadFileChangeFallback(value: unknown): ThreadFileChangeFall
579579 return normalized
580580}
581581
582- function buildTurnIndexByTurnId ( payload : ThreadReadResponse ) : ThreadTurnIndexById {
582+ function buildTurnIndexByTurnId ( payload : ThreadReadResponse , baseTurnIndex = 0 ) : ThreadTurnIndexById {
583583 const turns = Array . isArray ( payload . thread . turns ) ? payload . thread . turns : [ ]
584584 const lookup : ThreadTurnIndexById = { }
585585
586- for ( let turnIndex = 0 ; turnIndex < turns . length ; turnIndex += 1 ) {
587- const turn = turns [ turnIndex ]
586+ for ( let turnOffset = 0 ; turnOffset < turns . length ; turnOffset += 1 ) {
587+ const turnIndex = baseTurnIndex + turnOffset
588+ const turn = turns [ turnOffset ]
588589 if ( typeof turn ?. id !== 'string' || turn . id . length === 0 ) continue
589590 lookup [ turn . id ] = turnIndex
590591 }
591592
592593 return lookup
593594}
594595
596+ function readThreadTurnStartIndex ( payload : ThreadReadResponse ) : number {
597+ const record = asRecord ( payload )
598+ const raw = record ?. threadTurnStartIndex
599+ return Math . max ( 0 , Math . floor ( typeof raw === 'number' ? raw : 0 ) )
600+ }
601+
595602async function fetchThreadFileChangeFallback ( threadId : string ) : Promise < ThreadFileChangeFallbackEntry [ ] > {
596603 const response = await fetch ( `/codex-api/thread-file-change-fallback?threadId=${ encodeURIComponent ( threadId ) } ` )
597604 if ( ! response . ok ) {
@@ -693,6 +700,15 @@ export type ThreadGroupsPage = {
693700 nextCursor : string | null
694701}
695702
703+ export type ThreadTurnPage = {
704+ messages : UiMessage [ ]
705+ inProgress : boolean
706+ activeTurnId : string
707+ hasMoreOlder : boolean
708+ startTurnIndex : number
709+ turnIndexByTurnId : ThreadTurnIndexById
710+ }
711+
696712async function getThreadGroupsPageV2 ( cursor : string | null , limit : number ) : Promise < ThreadGroupsPage > {
697713 const payload = await callRpc < ThreadListResponse > ( 'thread/list' , {
698714 archived : false ,
@@ -714,7 +730,7 @@ async function getThreadMessagesV2(threadId: string): Promise<UiMessage[]> {
714730 threadId,
715731 includeTurns : true ,
716732 } )
717- return normalizeThreadMessagesV2 ( payload )
733+ return normalizeThreadMessagesV2 ( payload , readThreadTurnStartIndex ( payload ) )
718734}
719735
720736async function getThreadSummaryV2 ( threadId : string ) : Promise < UiThread > {
@@ -729,18 +745,51 @@ async function getThreadDetailV2(threadId: string): Promise<{
729745 messages : UiMessage [ ]
730746 inProgress : boolean
731747 activeTurnId : string
748+ hasMoreOlder : boolean
732749 turnIndexByTurnId : ThreadTurnIndexById
733750} > {
734751 const payload = await callRpc < ThreadReadResponse > ( 'thread/read' , {
735752 threadId,
736753 includeTurns : true ,
737754 } )
738- const normalized = normalizeThreadMessagesV2 ( payload )
755+ const startTurnIndex = readThreadTurnStartIndex ( payload )
756+ const normalized = normalizeThreadMessagesV2 ( payload , startTurnIndex )
739757 return {
740758 messages : normalized ,
741759 inProgress : readThreadInProgressFromResponse ( payload ) ,
742760 activeTurnId : readActiveTurnIdFromResponse ( payload ) ,
743- turnIndexByTurnId : buildTurnIndexByTurnId ( payload ) ,
761+ hasMoreOlder : startTurnIndex > 0 ,
762+ turnIndexByTurnId : buildTurnIndexByTurnId ( payload , startTurnIndex ) ,
763+ }
764+ }
765+
766+ async function getOlderThreadMessagesV2 ( threadId : string , beforeTurnId : string , limit = 10 ) : Promise < ThreadTurnPage > {
767+ const params = new URLSearchParams ( {
768+ threadId,
769+ beforeTurnId,
770+ limit : String ( limit ) ,
771+ } )
772+ const response = await fetch ( `/codex-api/thread-turn-page?${ params . toString ( ) } ` )
773+ if ( ! response . ok ) {
774+ throw new Error ( `Older thread page request failed with ${ response . status } ` )
775+ }
776+ const payload = await response . json ( ) as {
777+ result ?: ThreadReadResponse
778+ hasMoreOlder ?: unknown
779+ startTurnIndex ?: unknown
780+ }
781+ if ( ! payload . result ) {
782+ throw new Error ( 'Older thread page response did not include a thread result' )
783+ }
784+ const startTurnIndex = Math . max ( 0 , Math . floor ( typeof payload . startTurnIndex === 'number' ? payload . startTurnIndex : 0 ) )
785+
786+ return {
787+ messages : normalizeThreadMessagesV2 ( payload . result , startTurnIndex ) ,
788+ inProgress : readThreadInProgressFromResponse ( payload . result ) ,
789+ activeTurnId : readActiveTurnIdFromResponse ( payload . result ) ,
790+ hasMoreOlder : payload . hasMoreOlder === true ,
791+ startTurnIndex,
792+ turnIndexByTurnId : buildTurnIndexByTurnId ( payload . result , startTurnIndex ) ,
744793 }
745794}
746795
@@ -787,6 +836,7 @@ export async function getThreadDetail(threadId: string): Promise<{
787836 messages : UiMessage [ ]
788837 inProgress : boolean
789838 activeTurnId : string
839+ hasMoreOlder : boolean
790840 turnIndexByTurnId : ThreadTurnIndexById
791841} > {
792842 try {
@@ -796,6 +846,14 @@ export async function getThreadDetail(threadId: string): Promise<{
796846 }
797847}
798848
849+ export async function getOlderThreadMessages ( threadId : string , beforeTurnId : string , limit ?: number ) : Promise < ThreadTurnPage > {
850+ try {
851+ return await getOlderThreadMessagesV2 ( threadId , beforeTurnId , limit )
852+ } catch ( error ) {
853+ throw normalizeCodexApiError ( error , `Failed to load earlier messages for thread ${ threadId } ` , 'thread/read' )
854+ }
855+ }
856+
799857function normalizeReviewLine ( value : unknown ) : UiReviewLine | null {
800858 const record = asRecord ( value )
801859 if ( ! record ) return null
@@ -1352,17 +1410,21 @@ export type ResumedThread = {
13521410 messages : UiMessage [ ]
13531411 inProgress : boolean
13541412 activeTurnId : string
1413+ hasMoreOlder : boolean
13551414 turnIndexByTurnId : ThreadTurnIndexById
13561415}
13571416
13581417export async function resumeThread ( threadId : string ) : Promise < ResumedThread > {
13591418 const payload = await callRpc < ThreadResumeResponse > ( 'thread/resume' , { threadId } )
1419+ const startTurnIndex = readThreadTurnStartIndex ( payload )
1420+ const messages = normalizeThreadMessagesV2 ( payload , startTurnIndex )
13601421 return {
13611422 model : normalizeThreadModelFromPayload ( payload ) ,
1362- messages : normalizeThreadMessagesV2 ( payload ) ,
1423+ messages,
13631424 inProgress : readThreadInProgressFromResponse ( payload ) ,
13641425 activeTurnId : readActiveTurnIdFromResponse ( payload ) ,
1365- turnIndexByTurnId : buildTurnIndexByTurnId ( payload ) ,
1426+ hasMoreOlder : startTurnIndex > 0 ,
1427+ turnIndexByTurnId : buildTurnIndexByTurnId ( payload , startTurnIndex ) ,
13661428 }
13671429}
13681430
@@ -1376,7 +1438,7 @@ export async function renameThread(threadId: string, threadName: string): Promis
13761438
13771439export async function rollbackThread ( threadId : string , numTurns : number ) : Promise < UiMessage [ ] > {
13781440 const payload = await callRpc < ThreadReadResponse > ( 'thread/rollback' , { threadId, numTurns } )
1379- return normalizeThreadMessagesV2 ( payload )
1441+ return normalizeThreadMessagesV2 ( payload , readThreadTurnStartIndex ( payload ) )
13801442}
13811443
13821444export async function revertThreadFileChanges ( threadId : string , turnId : string , cwd : string ) : Promise < { reverted : number ; errors : string [ ] } > {
@@ -1483,7 +1545,7 @@ export async function forkThread(
14831545 threadId : forkedThreadId ,
14841546 cwd : normalizeThreadCwdFromPayload ( payload ) ,
14851547 model : normalizeThreadModelFromPayload ( payload ) ,
1486- messages : normalizeThreadMessagesV2 ( payload ) ,
1548+ messages : normalizeThreadMessagesV2 ( payload , readThreadTurnStartIndex ( payload ) ) ,
14871549 }
14881550 } catch ( error ) {
14891551 throw normalizeCodexApiError ( error , `Failed to fork thread ${ threadId } ` , 'thread/fork' )
0 commit comments