2026-05-15 10:08:52 +08:00
/**
* Context compression — build conversation history from DB,
* apply snapshot-aware compression and LLM summarization.
*/
import {
getSessionDetail ,
2026-05-19 16:09:59 +08:00
getSession ,
2026-05-15 10:08:52 +08:00
} from '../../../db/hermes/session-store'
import { getCompressionSnapshot } from '../../../db/hermes/compression-snapshot'
2026-05-15 13:50:27 +08:00
import { ChatContextCompressor , SUMMARY_PREFIX } from '../../../lib/context-compressor'
2026-05-15 10:08:52 +08:00
import { getModelContextLength } from '../model-context'
2026-05-19 17:58:39 +08:00
import { readConfigYamlForProfile } from '../../config-helpers'
2026-05-15 10:08:52 +08:00
import { logger } from '../../logger'
import { bridgeLogger } from '../../logger'
2026-05-22 09:46:50 +08:00
import { calcAndUpdateUsage , estimateUsageTokensFromMessages , updateMessageContextTokenUsage } from './usage'
2026-05-16 11:01:33 +08:00
import { isAssistantMessageSendable } from './message-format'
2026-05-19 17:58:39 +08:00
import type { ChatMessage , CompressionConfig as CompressorConfig } from '../../../lib/context-compressor'
2026-05-15 10:08:52 +08:00
import type { SessionState , BridgeCompressionResult } from './types'
2026-05-19 17:58:39 +08:00
interface RunChatCompressionConfig {
enabled : boolean
triggerTokens : number
compressor : Partial < CompressorConfig >
}
2026-05-21 19:40:52 +08:00
export class ContextWindowTooSmallError extends Error {
constructor ( message : string ) {
super ( message )
this . name = 'ContextWindowTooSmallError'
}
}
function isContextWindowTooSmallError ( err : unknown ) : err is ContextWindowTooSmallError {
return err instanceof ContextWindowTooSmallError || ( err instanceof Error && err . name === 'ContextWindowTooSmallError' )
}
2026-05-19 17:58:39 +08:00
function isSnapshotUsable (
snapshot : { lastMessageIndex : number } | null ,
history : ChatMessage [ ] ,
) : boolean {
return ! ! snapshot && snapshot . lastMessageIndex >= 0 && snapshot . lastMessageIndex < history . length
}
function buildSnapshotHistory (
snapshot : { summary : string ; lastMessageIndex : number } | null ,
history : ChatMessage [ ] ,
compressionConfig? : Partial < CompressorConfig > ,
) : ChatMessage [ ] | null {
if ( ! snapshot ) return null
const headCount = compressionConfig ? . headMessageCount || 0
const tailCount = compressionConfig ? . tailMessageCount || 0
const protectedHead = headCount > 0 ? history . slice ( 0 , headCount ) : [ ]
const summaryMessage = { role : 'user' , content : SUMMARY_PREFIX + '\n\n' + snapshot . summary } as ChatMessage
if ( isSnapshotUsable ( snapshot , history ) ) {
return [
. . . protectedHead ,
summaryMessage ,
. . . history . slice ( snapshot . lastMessageIndex + 1 ) ,
]
}
const tailStart = Math . max ( protectedHead . length , history . length - tailCount )
return [
. . . protectedHead ,
summaryMessage ,
. . . history . slice ( tailStart ) ,
]
}
2026-05-22 09:46:50 +08:00
export async function buildSnapshotAwareHistory (
sessionId : string ,
profile : string ,
history : ChatMessage [ ] ,
modelContext : { model? : string | null ; provider? : string | null } = { } ,
) : Promise < ChatMessage [ ] > {
const snapshot = getCompressionSnapshot ( sessionId )
if ( ! snapshot ) return history
const contextLength = getModelContextLength ( {
profile ,
model : modelContext.model ,
provider : modelContext.provider ,
} )
const compressionConfig = await getRunChatCompressionConfig ( profile , contextLength )
return buildSnapshotHistory ( snapshot , history , compressionConfig . compressor ) || history
}
2026-05-19 17:58:39 +08:00
function clampRatio ( value : unknown , fallback : number , min : number , max : number ) : number {
const n = typeof value === 'number' && Number . isFinite ( value ) ? value : fallback
return Math . min ( max , Math . max ( min , n ) )
}
function clampInt ( value : unknown , fallback : number , min : number , max : number ) : number {
const n = typeof value === 'number' && Number . isFinite ( value ) ? Math . floor ( value ) : fallback
return Math . min ( max , Math . max ( min , n ) )
}
async function getRunChatCompressionConfig ( profile : string , contextLength : number ) : Promise < RunChatCompressionConfig > {
let raw : Record < string , any > = { }
try {
raw = ( await readConfigYamlForProfile ( profile ) ) ? . compression || { }
} catch ( err ) {
logger . warn ( err , '[context-compress] failed to read compression config for profile %s, using defaults' , profile )
}
const threshold = clampRatio ( raw . threshold , 0.5 , 0.05 , 0.95 )
const targetRatio = clampRatio ( raw . target_ratio , 0.2 , 0.01 , 0.8 )
const protectLastN = clampInt ( raw . protect_last_n , 20 , 0 , 500 )
const protectFirstN = clampInt ( raw . protect_first_n , 3 , 0 , 100 )
return {
enabled : raw.enabled !== false ,
triggerTokens : Math.floor ( contextLength * threshold ) ,
compressor : {
triggerTokens : Math.floor ( contextLength * threshold ) ,
summaryBudget : Math.max ( 1 _000 , Math . floor ( contextLength * targetRatio ) ) ,
headMessageCount : protectFirstN ,
tailMessageCount : protectLastN ,
} ,
}
}
2026-05-15 10:08:52 +08:00
/**
* Load conversation history from DB with full message structure (user/assistant/tool).
*/
export async function buildDbHistory (
sessionId : string ,
options : { excludeLastUser? : boolean } = { } ,
) : Promise < ChatMessage [ ] > {
const detail = getSessionDetail ( sessionId )
if ( ! detail ? . messages ? . length ) return [ ]
const validMessages = detail . messages . filter ( m = >
( m . role === 'user' || m . role === 'assistant' || m . role === 'tool' ) && m . content !== undefined ,
)
const sourceMessages = options . excludeLastUser
? ( ( ) = > {
const lastUserMsgIndex = [ . . . validMessages ] . reverse ( ) . findIndex ( m = > m . role === 'user' )
return lastUserMsgIndex >= 0
? validMessages . slice ( 0 , validMessages . length - lastUserMsgIndex - 1 )
: validMessages
} ) ( )
: validMessages
return sourceMessages . map ( ( m , idx , arr ) = > {
const msg : any = { role : m.role , content : m.content || '' }
if ( m . reasoning_content ) msg . reasoning_content = m . reasoning_content
if ( m . tool_calls ? . length ) {
const cleanedToolCalls = m . tool_calls
. filter ( ( tc : any ) = > tc . id && tc . id . length > 0 )
. map ( ( tc : any ) = > ( { id : tc.id , type : tc . type , function : tc . function } ) )
if ( cleanedToolCalls . length > 0 ) msg . tool_calls = cleanedToolCalls
}
if ( m . role === 'tool' ) {
let callId = m . tool_call_id
if ( ! callId || callId . length === 0 ) {
const prevMsg = arr [ idx - 1 ]
if ( prevMsg ? . role === 'assistant' && prevMsg . tool_calls ? . length ) {
const tc = prevMsg . tool_calls . find ( ( t : any ) = > t . function ? . name === m . tool_name )
if ( tc ? . id ) callId = tc . id
}
}
if ( ! callId || callId . length === 0 ) return null
msg . tool_call_id = callId
}
if ( m . tool_name ) msg . name = m . tool_name
2026-05-16 11:01:33 +08:00
if ( m . role === 'assistant' && ! isAssistantMessageSendable ( msg ) ) {
logger . warn ( '[chat-run-socket] skipped empty assistant message while building history for session %s' , sessionId )
return null
}
2026-05-15 10:08:52 +08:00
return msg
} ) . filter ( ( m ) : m is ChatMessage = > m !== null )
}
2026-05-15 14:10:49 +08:00
export function estimateSnapshotAwareHistoryUsage (
sessionId : string ,
history : ChatMessage [ ] ,
) : { messageCount : number ; tokenCount : number } {
const snapshot = getCompressionSnapshot ( sessionId )
2026-05-19 17:58:39 +08:00
const messages = buildSnapshotHistory ( snapshot , history ) || history
2026-05-15 14:10:49 +08:00
const usage = estimateUsageTokensFromMessages ( messages )
return {
messageCount : messages.length ,
tokenCount : usage.inputTokens + usage . outputTokens ,
}
}
2026-05-15 10:08:52 +08:00
export async function buildCompressedHistory (
sessionId : string ,
profile : string ,
upstream : string ,
apiKey : string | undefined ,
emit : ( event : string , payload : any ) = > void ,
sessionMap : Map < string , SessionState > ,
2026-05-19 16:09:59 +08:00
modelContext : { model? : string | null ; provider? : string | null } = { } ,
2026-05-21 19:40:52 +08:00
contextTokenEstimator ? : ( messages : ChatMessage [ ] ) = > Promise < number | null | undefined > ,
2026-05-15 10:08:52 +08:00
) : Promise < ChatMessage [ ] > {
try {
let history = await buildDbHistory ( sessionId , { excludeLastUser : true } )
2026-05-19 16:09:59 +08:00
const contextLength = getModelContextLength ( {
profile ,
model : modelContext.model ,
provider : modelContext.provider ,
} )
2026-05-19 17:58:39 +08:00
const compressionConfig = await getRunChatCompressionConfig ( profile , contextLength )
const triggerTokens = compressionConfig . triggerTokens
if ( ! compressionConfig . enabled ) {
logger . info ( '[context-compress] session=%s: compression disabled by config' , sessionId )
return history
}
2026-05-15 10:08:52 +08:00
const cState = getOrCreateSession ( sessionMap , sessionId )
const assembledTokens = await calcAndUpdateUsage ( sessionId , cState , emit )
2026-05-21 19:40:52 +08:00
const estimateFullContextTokens = async ( messages : ChatMessage [ ] , fallback : number ) = > {
try {
const estimate = await contextTokenEstimator ? . ( messages )
if ( typeof estimate === 'number' && Number . isFinite ( estimate ) && estimate > 0 ) return Math . floor ( estimate )
} catch ( err ) {
logger . warn ( err , '[context-compress] session=%s: full context token estimate failed; using message-only estimate' , sessionId )
}
return fallback
}
const emitContextUsage = ( contextTokens : number ) = > {
cState . contextTokens = contextTokens
emit ( 'usage.updated' , {
event : 'usage.updated' ,
session_id : sessionId ,
inputTokens : cState.inputTokens ? ? assembledTokens . inputTokens ,
outputTokens : cState.outputTokens ? ? assembledTokens . outputTokens ,
contextTokens ,
} )
}
const messageOnlyTotalTokens = assembledTokens . inputTokens + assembledTokens . outputTokens
let totalTokens = messageOnlyTotalTokens
if ( history . length === 0 ) {
totalTokens = await estimateFullContextTokens ( [ ] , 0 )
if ( totalTokens > triggerTokens ) {
throw new ContextWindowTooSmallError (
` Context window is too small: system prompt and tool schemas already use ~ ${ totalTokens } tokens, exceeding compression threshold ${ triggerTokens } . Increase model context length, raise compression.threshold, or disable some tools. ` ,
)
}
if ( totalTokens > 0 ) emitContextUsage ( totalTokens )
return [ ]
}
const canCompressHistory = history . length > 4
2026-05-15 10:08:52 +08:00
const snapshot = getCompressionSnapshot ( sessionId )
2026-05-19 17:58:39 +08:00
const staleSnapshot = snapshot && ! isSnapshotUsable ( snapshot , history )
if ( staleSnapshot ) {
logger . warn ( '[context-compress] session=%s: stale snapshot index %d for %d history messages; using summary plus safe tail' ,
sessionId , snapshot . lastMessageIndex , history . length )
const staleHistory = buildSnapshotHistory ( snapshot , history , compressionConfig . compressor ) || history
const staleUsage = estimateUsageTokensFromMessages ( staleHistory )
2026-05-21 19:40:52 +08:00
totalTokens = await estimateFullContextTokens ( staleHistory , staleUsage . inputTokens + staleUsage . outputTokens )
emitContextUsage ( totalTokens )
logger . info ( {
sessionId ,
profile ,
messages : staleHistory.length ,
messageOnlyTokens : staleUsage.inputTokens + staleUsage . outputTokens ,
fullContextTokens : totalTokens ,
triggerTokens ,
decision : totalTokens > triggerTokens ? 'compress' : 'skip' ,
snapshot : 'stale' ,
} , '[context-compress] threshold check' )
2026-05-19 17:58:39 +08:00
}
2026-05-15 10:08:52 +08:00
2026-05-19 17:58:39 +08:00
if ( snapshot && ! staleSnapshot ) {
2026-05-15 10:08:52 +08:00
const newMessages = history . slice ( snapshot . lastMessageIndex + 1 )
2026-05-21 19:40:52 +08:00
const snapshotHistory = buildSnapshotHistory ( snapshot , history , compressionConfig . compressor ) || history
const snapshotUsage = estimateUsageTokensFromMessages ( snapshotHistory )
totalTokens = await estimateFullContextTokens ( snapshotHistory , snapshotUsage . inputTokens + snapshotUsage . outputTokens )
emitContextUsage ( totalTokens )
logger . info ( {
sessionId ,
profile ,
messages : snapshotHistory.length ,
messageOnlyTokens : snapshotUsage.inputTokens + snapshotUsage . outputTokens ,
fullContextTokens : totalTokens ,
triggerTokens ,
decision : totalTokens > triggerTokens ? 'compress' : 'skip' ,
snapshot : 'usable' ,
} , '[context-compress] threshold check' )
2026-05-15 10:08:52 +08:00
logger . info ( '[context-compress] session=%s: snapshot at %d, %d new messages, assembled ~%d tokens (threshold %d)' ,
sessionId , snapshot . lastMessageIndex , newMessages . length , totalTokens , triggerTokens )
2026-05-19 17:58:39 +08:00
if ( totalTokens <= triggerTokens ) {
2026-05-21 19:40:52 +08:00
history = snapshotHistory
2026-05-19 17:58:39 +08:00
} else {
history = await compressHistory ( history , newMessages , sessionId , upstream , apiKey , cState , totalTokens , emit , sessionMap , modelContext , compressionConfig . compressor )
}
} else if ( snapshot && staleSnapshot ) {
if ( totalTokens <= triggerTokens ) {
history = buildSnapshotHistory ( snapshot , history , compressionConfig . compressor ) || history
2026-05-15 10:08:52 +08:00
} else {
2026-05-19 17:58:39 +08:00
history = await compressHistory ( history , null , sessionId , upstream , apiKey , cState , totalTokens , emit , sessionMap , modelContext , compressionConfig . compressor )
2026-05-15 10:08:52 +08:00
}
2026-05-21 19:40:52 +08:00
} else {
totalTokens = await estimateFullContextTokens ( history , totalTokens )
emitContextUsage ( totalTokens )
logger . info ( {
sessionId ,
profile ,
messages : history.length ,
messageOnlyTokens : messageOnlyTotalTokens ,
fullContextTokens : totalTokens ,
triggerTokens ,
decision : totalTokens > triggerTokens ? 'compress' : 'skip' ,
snapshot : 'none' ,
} , '[context-compress] threshold check' )
if ( ! canCompressHistory && totalTokens > triggerTokens ) {
throw new ContextWindowTooSmallError (
` Context window is too small: fixed prompt/tool overhead plus ${ history . length } history messages uses ~ ${ totalTokens } tokens, exceeding compression threshold ${ triggerTokens } , and there is not enough history to compress. Increase model context length, raise compression.threshold, or disable some tools. ` ,
)
}
2026-05-19 17:58:39 +08:00
if ( totalTokens <= triggerTokens ) {
2026-05-15 10:08:52 +08:00
logger . info ( '[context-compress] session=%s: %d messages, ~%d tokens — under threshold, skip' , sessionId , history . length , totalTokens )
} else {
2026-05-19 17:58:39 +08:00
history = await compressHistory ( history , null , sessionId , upstream , apiKey , cState , totalTokens , emit , sessionMap , modelContext , compressionConfig . compressor )
2026-05-15 10:08:52 +08:00
}
}
return history
} catch ( err ) {
2026-05-21 19:40:52 +08:00
if ( isContextWindowTooSmallError ( err ) ) throw err
2026-05-15 10:08:52 +08:00
logger . warn ( err , '[chat-run-socket] failed to build compressed history for session %s' , sessionId )
return [ ]
}
}
export async function compressHistory (
history : ChatMessage [ ] ,
newMessagesOnly : ChatMessage [ ] | null ,
sessionId : string ,
upstream : string ,
apiKey : string | undefined ,
cState : SessionState ,
totalTokens : number ,
emit : ( event : string , payload : any ) = > void ,
sessionMap : Map < string , SessionState > ,
2026-05-19 16:09:59 +08:00
modelContext : { model? : string | null ; provider? : string | null } = { } ,
2026-05-19 17:58:39 +08:00
compressionConfig? : Partial < CompressorConfig > ,
2026-05-15 10:08:52 +08:00
) : Promise < ChatMessage [ ] > {
const msgCount = newMessagesOnly ? newMessagesOnly.length : history.length
pushState ( sessionMap , sessionId , 'compression.started' , {
event : 'compression.started' , message_count : msgCount , token_count : totalTokens ,
} )
emit ( 'compression.started' , {
event : 'compression.started' , message_count : msgCount , token_count : totalTokens ,
} )
try {
2026-05-19 16:09:59 +08:00
const session = getSession ( sessionId )
2026-05-19 17:58:39 +08:00
const compressor = new ChatContextCompressor ( { config : compressionConfig } )
2026-05-19 16:09:59 +08:00
const result = await compressor . compress ( history , upstream , apiKey , sessionId , {
profile : session?.profile ,
model : modelContext.model || session ? . model ,
provider : modelContext.provider || session ? . provider ,
} )
2026-05-15 10:08:52 +08:00
const afterTokens = await calcAndUpdateUsage ( sessionId , cState , emit )
2026-05-22 09:46:50 +08:00
const compressedAfterTokens = afterTokens . inputTokens + afterTokens . outputTokens
const compressedMeta : any = {
2026-05-15 10:08:52 +08:00
event : 'compression.completed' as const ,
compressed : result.meta.compressed ,
llmCompressed : result.meta.llmCompressed ,
totalMessages : result.meta.totalMessages ,
resultMessages : result.messages.length ,
beforeTokens : totalTokens ,
2026-05-22 09:46:50 +08:00
afterTokens : compressedAfterTokens ,
2026-05-15 10:08:52 +08:00
summaryTokens : result.meta.summaryTokenEstimate ,
verbatimCount : result.meta.verbatimCount ,
compressedStartIndex : result.meta.compressedStartIndex ,
}
replaceState ( sessionMap , sessionId , 'compression.completed' , compressedMeta )
logger . info ( '[context-compress] AFTER session=%s: %d messages, ~%d tokens (was %d)' ,
2026-05-22 09:46:50 +08:00
sessionId , result . messages . length , compressedAfterTokens , totalTokens )
const compressedContextTokens = updateMessageContextTokenUsage ( sessionId , cState , emit , compressedAfterTokens , afterTokens )
if ( compressedContextTokens != null ) {
compressedMeta . contextTokens = compressedContextTokens
}
2026-05-15 10:08:52 +08:00
emit ( 'compression.completed' , compressedMeta )
const compressed = result . messages . map ( m = > {
const msg : any = { role : m.role , content : m.content , tool_call_id : m.tool_call_id , name : m.name }
if ( m . reasoning_content ) msg . reasoning_content = m . reasoning_content
if ( m . tool_calls ? . length ) {
const cleanedToolCalls = m . tool_calls
. filter ( ( tc : any ) = > tc . id && tc . id . length > 0 )
. map ( ( tc : any ) = > ( { id : tc.id , type : tc . type , function : tc . function } ) )
if ( cleanedToolCalls . length > 0 ) msg . tool_calls = cleanedToolCalls
}
return msg
} )
await calcAndUpdateUsage ( sessionId , cState , emit )
return compressed
} catch ( err : any ) {
const failedMeta = {
event : 'compression.completed' as const ,
compressed : false ,
totalMessages : msgCount ,
resultMessages : msgCount ,
beforeTokens : totalTokens ,
afterTokens : totalTokens ,
summaryTokens : 0 ,
verbatimCount : msgCount ,
compressedStartIndex : - 1 ,
error : err.message ,
}
replaceState ( sessionMap , sessionId , 'compression.completed' , failedMeta )
logger . warn ( err , '[chat-run-socket] compression failed for session %s, using assembled context' , sessionId )
emit ( 'compression.completed' , failedMeta )
return history
}
}
export async function forceCompressBridgeHistory (
sessionId : string ,
profile : string ,
_messages : ChatMessage [ ] ,
2026-05-21 19:40:52 +08:00
beforeTokenOverride? : number | null ,
2026-05-15 10:08:52 +08:00
) : Promise < BridgeCompressionResult > {
const history = await buildDbHistory ( sessionId , { excludeLastUser : true } )
if ( history . length === 0 ) {
return {
messages : [ ] ,
beforeMessages : 0 ,
resultMessages : 0 ,
beforeTokens : 0 ,
afterTokens : 0 ,
compressed : false ,
llmCompressed : false ,
summaryTokens : 0 ,
verbatimCount : 0 ,
compressedStartIndex : - 1 ,
}
}
2026-05-19 16:09:59 +08:00
const upstream = ''
const apiKey = undefined
const session = getSession ( sessionId )
2026-05-19 17:58:39 +08:00
const contextLength = getModelContextLength ( { profile , model : session?.model , provider : session?.provider } )
const compressionConfig = await getRunChatCompressionConfig ( session ? . profile || profile , contextLength )
2026-05-15 14:10:49 +08:00
const beforeUsage = estimateSnapshotAwareHistoryUsage ( sessionId , history )
2026-05-21 19:40:52 +08:00
const totalTokens = typeof beforeTokenOverride === 'number' && Number . isFinite ( beforeTokenOverride ) && beforeTokenOverride > 0
? Math . floor ( beforeTokenOverride )
: beforeUsage . tokenCount
2026-05-15 10:08:52 +08:00
bridgeLogger . info ( {
sessionId ,
profile ,
historyMessages : history.length ,
2026-05-15 14:10:49 +08:00
snapshotAwareMessages : beforeUsage.messageCount ,
2026-05-15 10:08:52 +08:00
bridgeProvidedMessages : Array.isArray ( _messages ) ? _messages.length : 0 ,
tokenEstimate : totalTokens ,
snapshotAware : true ,
} , '[chat-run-socket] bridge forced compression started' )
2026-05-19 17:58:39 +08:00
const compressor = new ChatContextCompressor ( { config : compressionConfig.compressor } )
2026-05-19 16:09:59 +08:00
const result = await compressor . compress ( history , upstream , apiKey , sessionId , {
profile : session?.profile || profile ,
model : session?.model ,
provider : session?.provider ,
} )
2026-05-15 10:08:52 +08:00
const compressedMessages = result . messages . map ( m = > {
const msg : any = { role : m.role , content : m.content }
if ( m . reasoning_content ) msg . reasoning_content = m . reasoning_content
if ( m . tool_calls ? . length ) {
const cleanedToolCalls = m . tool_calls
. filter ( ( tc : any ) = > tc . id && tc . id . length > 0 )
. map ( ( tc : any ) = > ( { id : tc.id , type : tc . type , function : tc . function } ) )
if ( cleanedToolCalls . length > 0 ) msg . tool_calls = cleanedToolCalls
}
if ( m . tool_call_id ) msg . tool_call_id = m . tool_call_id
if ( m . name ) msg . name = m . name
return msg
} )
2026-05-15 13:50:27 +08:00
const afterUsage = estimateUsageTokensFromMessages ( compressedMessages )
const afterTokens = afterUsage . inputTokens + afterUsage . outputTokens
2026-05-15 10:08:52 +08:00
bridgeLogger . info ( {
sessionId ,
profile ,
beforeMessages : history.length ,
resultMessages : result.messages.length ,
beforeTokens : totalTokens ,
afterTokens ,
compressed : result.meta.compressed ,
llmCompressed : result.meta.llmCompressed ,
verbatimCount : result.meta.verbatimCount ,
compressedStartIndex : result.meta.compressedStartIndex ,
compressedHistory : result.messages.map ( ( m ) = > ( {
role : m.role ,
content : m.content ,
reasoning_content : m.reasoning_content ,
tool_calls : m.tool_calls ,
tool_call_id : m.tool_call_id ,
name : m.name ,
} ) ) ,
} , '[chat-run-socket] bridge forced compression completed' )
return {
messages : compressedMessages ,
beforeMessages : history.length ,
resultMessages : compressedMessages.length ,
beforeTokens : totalTokens ,
afterTokens ,
compressed : result.meta.compressed ,
llmCompressed : result.meta.llmCompressed ,
summaryTokens : result.meta.summaryTokenEstimate ,
verbatimCount : result.meta.verbatimCount ,
compressedStartIndex : result.meta.compressedStartIndex ,
}
}
// --- Shared state helpers (used by compression) ---
export function getOrCreateSession ( sessionMap : Map < string , SessionState > , sessionId : string ) : SessionState {
let state = sessionMap . get ( sessionId )
if ( ! state ) {
state = { messages : [ ] , isWorking : false , events : [ ] , queue : [ ] }
sessionMap . set ( sessionId , state )
}
return state
}
export function pushState ( sessionMap : Map < string , SessionState > , sessionId : string , event : string , data : any ) {
const state = getOrCreateSession ( sessionMap , sessionId )
state . events . push ( { event , data } )
}
export function replaceState ( sessionMap : Map < string , SessionState > , sessionId : string , event : string , data : any ) {
const state = sessionMap . get ( sessionId )
if ( state ) {
const idx = state . events . findIndex ( s = > s . event === event )
if ( idx >= 0 ) {
state . events [ idx ] = { event , data }
return
}
}
pushState ( sessionMap , sessionId , event , data )
}