@@ -20,7 +20,8 @@ export type SSEEventType =
2020 | 'agent_list'
2121 | 'agent_switched'
2222 | 'todo_update'
23- | 'todos' ;
23+ | 'todos'
24+ | 'sub_agent_message' ;
2425
2526/**
2627 * SSE 事件数据结构
@@ -56,6 +57,7 @@ export interface ClientMessage {
5657 agentId ?: string ; // 目标主代理ID,用于 switch_agent 消息
5758 rollback ?: {
5859 messageIndex : number ;
60+ snapshotIndex ?: number ; // UI 消息索引, 用于快照操作(与 messageIndex 属不同索引体系)
5961 rollbackFiles : boolean ;
6062 selectedFiles ?: string [ ] ;
6163 crossSessionRollback ?: boolean ;
@@ -545,6 +547,9 @@ export class SSEServer {
545547 const { hashBasedSnapshotManager} = await import (
546548 '../utils/codebase/hashBasedSnapshot.js'
547549 ) ;
550+ const { convertSessionMessagesToUI} = await import (
551+ '../utils/session/sessionConverter.js'
552+ ) ;
548553
549554 const sessionIdRaw = query ?. [ 'sessionId' ] ;
550555 const sessionId =
@@ -568,6 +573,30 @@ export class SSEServer {
568573 return ;
569574 }
570575
576+ // 快照系统使用 UI 消息索引(convertSessionMessagesToUI 后的长度)
577+ // 而非原始 session.messages 索引, 两者不一致(UI 转换会合并/过滤 tool 等消息)
578+ const uiMessages = convertSessionMessagesToUI ( session . messages ) ;
579+
580+ // 构建 "原始 user 消息索引 → 对应 UI 消息索引" 映射
581+ // 快照在 tool 执行期间创建, messageIndex = 当时的 uiMessages.length
582+ // 即: user 消息保存后, uiMessages 中该 user 位于 uiIdx, 快照记录 uiIdx + 1
583+ const rawToUiIndexMap = new Map < number , number > ( ) ;
584+ let uiIdx = 0 ;
585+ for ( let i = 0 ; i < session . messages . length ; i ++ ) {
586+ const msg : any = session . messages [ i ] ;
587+ if ( ! msg || msg . role !== 'user' ) continue ;
588+ while (
589+ uiIdx < uiMessages . length &&
590+ uiMessages [ uiIdx ] ! . role !== 'user'
591+ ) {
592+ uiIdx ++ ;
593+ }
594+ if ( uiIdx < uiMessages . length ) {
595+ rawToUiIndexMap . set ( i , uiIdx + 1 ) ;
596+ uiIdx ++ ;
597+ }
598+ }
599+
571600 const snapshots = await hashBasedSnapshotManager . listSnapshots (
572601 sessionId ,
573602 ) ;
@@ -584,9 +613,11 @@ export class SSEServer {
584613
585614 const points : Array < {
586615 messageIndex : number ;
616+ snapshotIndex ?: number ;
587617 role : 'user' ;
588618 timestamp : number ;
589619 summary : string ;
620+ content : string ;
590621 hasSnapshot : boolean ;
591622 snapshot ?: { timestamp : number ; fileCount : number } ;
592623 filesToRollbackCount : number ;
@@ -603,40 +634,31 @@ export class SSEServer {
603634 ? normalized . slice ( 0 , maxSummaryLen ) + '…'
604635 : normalized ;
605636
606- // Snapshot 的 messageIndex 和 session.messages 的索引并不总是一致。
607- // 实测快照通常对应“下一条消息写入前”的索引(例如首条 user 消息后快照会落在 1)。
608- const snapAtNext = snapshotByIndex . get ( i + 1 ) ;
609- const snapAtCurrent = snapshotByIndex . get ( i ) ;
610- const snap = snapAtNext ?? snapAtCurrent ;
611- const rollbackIndex = snapAtNext ? i + 1 : i ;
637+ // 使用 UI 消息索引查找快照(快照系统记录的是 UI 索引)
638+ // 映射缺失时(uiIndex === -1)表示该 user 消息无 UI 对应, 跳过快照查找
639+ const uiIndex = rawToUiIndexMap . get ( i ) ?? - 1 ;
640+ const snap = uiIndex >= 0 ? snapshotByIndex . get ( uiIndex ) : undefined ;
612641
613642 let filesToRollbackCount = 0 ;
614643 if ( snap && snap . fileCount > 0 ) {
615644 const files = await hashBasedSnapshotManager . getFilesToRollback (
616645 sessionId ,
617- rollbackIndex ,
646+ uiIndex ,
618647 ) ;
619648 filesToRollbackCount = Array . isArray ( files ) ? files . length : 0 ;
620649 }
621650
622651 points . push ( {
623652 messageIndex : i ,
653+ ...( uiIndex >= 0 ? { snapshotIndex : uiIndex } : { } ) ,
624654 role : 'user' ,
625655 timestamp : typeof m . timestamp === 'number' ? m . timestamp : 0 ,
626656 summary,
657+ content,
627658 hasSnapshot : ! ! snap && snap . fileCount > 0 ,
628659 snapshot : snap ,
629660 filesToRollbackCount,
630661 } ) ;
631-
632- // 如果快照存在但落在 i+1(常见),让前端能直接用 messageIndex 作为回滚点索引。
633- if (
634- snapAtNext &&
635- snapAtNext . fileCount > 0 &&
636- i + 1 < session . messages . length
637- ) {
638- // 这里不改变 messageIndex 的语义,仅用于确保 hasSnapshot 展示正确。
639- }
640662 }
641663
642664 res . writeHead ( 200 , {
@@ -782,6 +804,7 @@ export class SSEServer {
782804 content : string ;
783805 [ key : string ] : any ;
784806 } > ;
807+ let currentSession : any = null ;
785808
786809 // 支持两种方式:直接传入 messages 或通过 sessionId 获取
787810 if ( body . messages && Array . isArray ( body . messages ) ) {
@@ -799,6 +822,7 @@ export class SSEServer {
799822 return ;
800823 }
801824 messages = session . messages || [ ] ;
825+ currentSession = session ;
802826 } else {
803827 res . writeHead ( 400 , {
804828 'Content-Type' : 'application/json' ,
@@ -856,11 +880,82 @@ export class SSEServer {
856880 return ;
857881 }
858882
883+ // Build new session messages with compression summary
884+ const newSessionMessages : Array < any > = [ ] ;
885+ let finalContent = `[Context Summary from Previous Conversation]\n\n${ result . summary } ` ;
886+
887+ if ( result . preservedMessages && result . preservedMessages . length > 0 ) {
888+ finalContent +=
889+ '\n\n---\n\n[Last Interaction - Preserved for Continuity]\n\n' ;
890+ for ( const msg of result . preservedMessages ) {
891+ if ( msg . role === 'user' ) {
892+ finalContent += `**User:**\n${ msg . content } \n\n` ;
893+ } else if ( msg . role === 'assistant' ) {
894+ finalContent += `**Assistant:**\n${ msg . content } ` ;
895+ if ( msg . tool_calls && msg . tool_calls . length > 0 ) {
896+ finalContent += '\n\n**[Tool Calls Initiated]:**\n```json\n' ;
897+ finalContent += JSON . stringify ( msg . tool_calls , null , 2 ) ;
898+ finalContent += '\n```\n\n' ;
899+ } else {
900+ finalContent += '\n\n' ;
901+ }
902+ } else if ( msg . role === 'tool' ) {
903+ finalContent += `**[Tool Result - ${ msg . tool_call_id } ]:**\n` ;
904+ try {
905+ const parsed = JSON . parse ( msg . content ) ;
906+ finalContent +=
907+ '```json\n' + JSON . stringify ( parsed , null , 2 ) + '\n```\n\n' ;
908+ } catch {
909+ finalContent += `${ msg . content } \n\n` ;
910+ }
911+ }
912+ }
913+ }
914+
915+ newSessionMessages . push ( {
916+ role : 'user' ,
917+ content : finalContent ,
918+ timestamp : Date . now ( ) ,
919+ } ) ;
920+
921+ // Create new compressed session
922+ const compressedSession = await sessionManager . createNewSession (
923+ false ,
924+ true ,
925+ ) ;
926+
927+ // Set session properties
928+ compressedSession . messages = newSessionMessages ;
929+ compressedSession . messageCount = newSessionMessages . length ;
930+ compressedSession . updatedAt = Date . now ( ) ;
931+ compressedSession . title = currentSession ?. title || 'Compressed Session' ;
932+ compressedSession . summary = currentSession ?. summary || '' ;
933+
934+ // Record compression relationship
935+ const sourceSessionId = body . sessionId ;
936+ compressedSession . compressedFrom = sourceSessionId ;
937+ compressedSession . compressedAt = Date . now ( ) ;
938+ compressedSession . originalMessageIndex =
939+ result . preservedMessageStartIndex ;
940+
941+ // Save new session to disk
942+ await sessionManager . saveSession ( compressedSession ) ;
943+
944+ // Return full response with new session info
859945 res . writeHead ( 200 , {
860946 'Content-Type' : 'application/json' ,
861947 'Access-Control-Allow-Origin' : '*' ,
862948 } ) ;
863- res . end ( JSON . stringify ( { success : true , result} ) ) ;
949+ res . end (
950+ JSON . stringify ( {
951+ success : true ,
952+ result : {
953+ ...result ,
954+ sessionId : compressedSession . id ,
955+ compressedFrom : sourceSessionId ,
956+ } ,
957+ } ) ,
958+ ) ;
864959 } catch ( error ) {
865960 res . writeHead ( 500 , {
866961 'Content-Type' : 'application/json' ,
0 commit comments