2026-06-09 19:31:31 +00:00
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
2026-06-03 02:42:13 +00:00
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
2026-06-01 05:00:40 +00:00
import { Session } from "./session"
2026-03-11 23:40:50 +00:00
import { SessionID , MessageID , PartID } from "./schema"
2026-04-27 18:33:33 +00:00
import { Provider } from "@/provider/provider"
2025-09-13 09:46:14 +00:00
import { MessageV2 } from "./message-v2"
2026-04-27 18:33:33 +00:00
import { Token } from "@/util/token"
2025-11-17 15:57:18 +00:00
import { SessionProcessor } from "./processor"
2025-12-15 02:11:30 +00:00
import { Agent } from "@/agent/agent"
2025-12-17 19:57:09 +00:00
import { Plugin } from "@/plugin"
2026-04-27 18:33:33 +00:00
import { Config } from "@/config/config"
import { NotFoundError } from "@/storage/storage"
2026-05-31 01:08:38 +00:00
2026-06-04 23:24:30 +00:00
import { Effect , Layer , Context } from "effect"
2026-04-27 18:33:33 +00:00
import { InstanceState } from "@/effect/instance-state"
2026-04-17 03:50:17 +00:00
import { isOverflow as overflow , usable } from "./overflow"
2026-05-23 23:48:08 +00:00
import { serviceUse } from "@opencode-ai/core/effect/service-use"
2026-05-13 12:14:09 +00:00
import { RuntimeFlags } from "@/effect/runtime-flags"
2026-05-15 00:50:23 +00:00
import { EventV2Bridge } from "@/event-v2-bridge"
2026-05-31 01:08:38 +00:00
import { ProviderV2 } from "@opencode-ai/core/provider"
2026-06-04 06:57:43 +00:00
import { ModelV2 } from "@opencode-ai/core/model"
2026-06-05 17:17:23 +00:00
import { buildPrompt } from "@opencode-ai/core/session/compaction"
2026-06-24 20:43:17 +00:00
import { SessionCompactionEvent } from "@opencode-ai/schema/session-compaction-event"
2025-09-13 09:46:14 +00:00
2026-06-24 20:43:17 +00:00
export const Event = SessionCompactionEvent
2026-04-17 00:49:39 +00:00
export const PRUNE_MINIMUM = 20 _000
export const PRUNE_PROTECT = 40 _000
2026-04-22 15:07:32 +00:00
const TOOL_OUTPUT_MAX_CHARS = 2 _000
2026-04-17 00:49:39 +00:00
const PRUNE_PROTECTED_TOOLS = [ "skill" ]
2026-04-20 06:06:29 +00:00
const MIN_PRESERVE_RECENT_TOKENS = 2 _000
2026-08-12 19:36:51 +00:00
const MAX_PRESERVE_RECENT_TOKENS = 15 _000
2026-04-17 03:50:17 +00:00
type Turn = {
start : number
end : number
2026-05-12 20:33:16 +00:00
id : MessageID
2026-04-17 03:50:17 +00:00
}
2026-04-10 04:04:01 +00:00
2026-04-22 15:07:32 +00:00
type Tail = {
start : number
2026-05-12 20:33:16 +00:00
id : MessageID
2026-04-22 15:07:32 +00:00
}
type CompletedCompaction = {
userIndex : number
assistantIndex : number
summary : string | undefined
}
2026-08-07 02:11:14 +00:00
const truncate = ( value : string ) = >
value . length <= TOOL_OUTPUT_MAX_CHARS ? value : ` ${ value . slice ( 0 , TOOL_OUTPUT_MAX_CHARS ) } \ n[truncated] `
const serialize = ( message : SessionV1.WithParts ) = > {
if ( message . info . role === "user" ) {
const text = message . parts
. filter ( ( part ) : part is SessionV1 . TextPart = > part . type === "text" && ! part . ignored )
. map ( ( part ) = > part . text )
. filter ( Boolean )
. join ( "\n" )
const files = message . parts . flatMap ( ( part ) = >
part . type === "file" ? [ ` [Attached ${ part . mime } : ${ part . filename ? ? "file" } ] ` ] : [ ] ,
)
return [ . . . ( text ? [ ` [User]: ${ text } ` ] : [ ] ) , . . . files ] . join ( "\n" )
}
return message . parts
. flatMap ( ( part ) = > {
if ( part . type === "text" ) return part . text ? [ ` [Assistant]: ${ part . text } ` ] : [ ]
if ( part . type === "reasoning" ) return part . text ? [ ` [Assistant reasoning]: ${ part . text } ` ] : [ ]
if ( part . type !== "tool" ) return [ ]
const call = ` [Assistant tool call]: ${ part . tool } ( ${ JSON . stringify ( part . state . input ) } ) `
if ( part . state . status === "completed" ) {
const attachments = ( part . state . attachments ? ? [ ] ) . map (
( item ) = > ` [Attached ${ item . mime } : ${ item . filename ? ? "file" } ] ` ,
)
const output = part . state . time . compacted
? "[Old tool result content cleared]"
: truncate ( [ part . state . output , . . . attachments ] . join ( "\n" ) )
return [ call , ` [Tool result]: ${ output } ` ]
}
if ( part . state . status === "error" ) return [ call , ` [Tool error]: ${ part . state . error } ` ]
return [ call ]
} )
. join ( "\n" )
}
2026-06-03 02:42:13 +00:00
function summaryText ( message : SessionV1.WithParts ) {
2026-04-22 15:07:32 +00:00
const text = message . parts
2026-06-03 02:42:13 +00:00
. filter ( ( part ) : part is SessionV1 . TextPart = > part . type === "text" )
2026-04-22 15:07:32 +00:00
. map ( ( part ) = > part . text . trim ( ) )
. filter ( Boolean )
. join ( "\n\n" )
. trim ( )
return text || undefined
}
2026-06-03 02:42:13 +00:00
function completedCompactions ( messages : SessionV1.WithParts [ ] ) {
2026-04-22 15:07:32 +00:00
const users = new Map < MessageID , number > ( )
for ( let i = 0 ; i < messages . length ; i ++ ) {
const msg = messages [ i ]
if ( msg . info . role !== "user" ) continue
if ( ! msg . parts . some ( ( part ) = > part . type === "compaction" ) ) continue
users . set ( msg . info . id , i )
}
return messages . flatMap ( ( msg , assistantIndex ) : CompletedCompaction [ ] = > {
if ( msg . info . role !== "assistant" ) return [ ]
if ( ! msg . info . summary || ! msg . info . finish || msg . info . error ) return [ ]
const userIndex = users . get ( msg . info . parentID )
if ( userIndex === undefined ) return [ ]
return [ { userIndex , assistantIndex , summary : summaryText ( msg ) } ]
} )
}
2026-06-03 02:42:13 +00:00
function preserveRecentBudget ( input : { cfg : ConfigV1.Info ; model : Provider.Model } ) {
2026-04-17 03:50:17 +00:00
return (
2026-04-20 06:06:29 +00:00
input . cfg . compaction ? . preserve_recent_tokens ? ?
2026-04-20 06:07:28 +00:00
Math . min ( MAX_PRESERVE_RECENT_TOKENS , Math . max ( MIN_PRESERVE_RECENT_TOKENS , Math . floor ( usable ( input ) * 0.25 ) ) )
2026-04-17 03:50:17 +00:00
)
}
2026-02-11 01:55:22 +00:00
2026-06-03 02:42:13 +00:00
function turns ( messages : SessionV1.WithParts [ ] ) {
2026-04-17 03:50:17 +00:00
const result : Turn [ ] = [ ]
for ( let i = 0 ; i < messages . length ; i ++ ) {
const msg = messages [ i ]
if ( msg . info . role !== "user" ) continue
if ( msg . parts . some ( ( part ) = > part . type === "compaction" ) ) continue
result . push ( {
start : i ,
end : messages.length ,
2026-05-12 20:33:16 +00:00
id : msg.info.id ,
2026-04-17 03:50:17 +00:00
} )
2026-04-10 04:06:39 +00:00
}
2026-04-17 03:50:17 +00:00
for ( let i = 0 ; i < result . length - 1 ; i ++ ) {
result [ i ] . end = result [ i + 1 ] . start
2025-09-13 09:46:14 +00:00
}
2026-04-17 03:50:17 +00:00
return result
}
2026-04-17 00:49:39 +00:00
2026-04-22 15:07:32 +00:00
function splitTurn ( input : {
2026-06-03 02:42:13 +00:00
messages : SessionV1.WithParts [ ]
2026-04-22 15:07:32 +00:00
turn : Turn
model : Provider.Model
budget : number
2026-06-03 02:42:13 +00:00
estimate : ( input : { messages : SessionV1.WithParts [ ] ; model : Provider.Model } ) = > Effect . Effect < number >
2026-04-22 15:07:32 +00:00
} ) {
return Effect . gen ( function * ( ) {
if ( input . budget <= 0 ) return undefined
if ( input . turn . end - input . turn . start <= 1 ) return undefined
for ( let start = input . turn . start + 1 ; start < input . turn . end ; start ++ ) {
const size = yield * input . estimate ( {
messages : input.messages.slice ( start , input . turn . end ) ,
model : input.model ,
} )
if ( size > input . budget ) continue
return {
start ,
2026-05-12 20:33:16 +00:00
id : input.messages [ start ] ! . info . id ,
2026-04-22 15:07:32 +00:00
} satisfies Tail
}
return undefined
} )
}
2026-04-17 00:49:39 +00:00
export interface Interface {
readonly isOverflow : ( input : {
2026-06-03 02:42:13 +00:00
tokens : SessionV1.Assistant [ "tokens" ]
2026-04-17 00:49:39 +00:00
model : Provider.Model
} ) = > Effect . Effect < boolean >
readonly prune : ( input : { sessionID : SessionID } ) = > Effect . Effect < void >
readonly process : ( input : {
parentID : MessageID
2026-06-03 02:42:13 +00:00
messages : SessionV1.WithParts [ ]
2026-04-17 00:49:39 +00:00
sessionID : SessionID
auto : boolean
overflow? : boolean
} ) = > Effect . Effect < "continue" | "stop" >
readonly create : ( input : {
sessionID : SessionID
agent : string
2026-06-04 06:57:43 +00:00
model : { providerID : ProviderV2.ID ; modelID : ModelV2.ID }
2026-04-17 00:49:39 +00:00
auto : boolean
overflow? : boolean
} ) = > Effect . Effect < void >
}
export class Service extends Context . Service < Service , Interface > ( ) ( "@opencode/SessionCompaction" ) { }
2026-05-10 16:12:00 +00:00
export const use = serviceUse ( Service )
2026-06-30 03:03:57 +00:00
const layer = Layer . effect (
2026-04-17 00:49:39 +00:00
Service ,
Effect . gen ( function * ( ) {
const config = yield * Config . Service
const session = yield * Session . Service
const agents = yield * Agent . Service
const plugin = yield * Plugin . Service
const processors = yield * SessionProcessor . Service
const provider = yield * Provider . Service
2026-05-15 00:50:23 +00:00
const events = yield * EventV2Bridge . Service
2026-05-13 12:14:09 +00:00
const flags = yield * RuntimeFlags . Service
2026-04-17 00:49:39 +00:00
const isOverflow = Effect . fn ( "SessionCompaction.isOverflow" ) ( function * ( input : {
2026-06-03 02:42:13 +00:00
tokens : SessionV1.Assistant [ "tokens" ]
2026-03-28 01:09:14 +00:00
model : Provider.Model
2026-04-17 00:49:39 +00:00
} ) {
2026-05-15 07:37:35 +00:00
return overflow ( {
cfg : yield * config . get ( ) ,
tokens : input.tokens ,
model : input.model ,
outputTokenMax : flags.outputTokenMax ,
} )
2026-04-17 00:49:39 +00:00
} )
2026-04-17 03:50:17 +00:00
const estimate = Effect . fn ( "SessionCompaction.estimate" ) ( function * ( input : {
2026-06-03 02:42:13 +00:00
messages : SessionV1.WithParts [ ]
2026-04-17 03:50:17 +00:00
model : Provider.Model
} ) {
2026-05-12 20:33:16 +00:00
const msgs = yield * MessageV2 . toModelMessagesEffect ( input . messages , input . model )
return Token . estimate ( JSON . stringify ( msgs ) )
2026-04-17 03:50:17 +00:00
} )
2026-03-28 01:09:14 +00:00
2026-04-17 03:50:17 +00:00
const select = Effect . fn ( "SessionCompaction.select" ) ( function * ( input : {
2026-06-03 02:42:13 +00:00
messages : SessionV1.WithParts [ ]
cfg : ConfigV1.Info
2026-04-17 03:50:17 +00:00
model : Provider.Model
} ) {
2026-08-12 19:36:51 +00:00
const limit = input . cfg . compaction ? . tail_turns
if ( limit !== undefined && limit <= 0 ) return { head : input.messages , tail_start_id : undefined }
2026-04-20 06:06:29 +00:00
const budget = preserveRecentBudget ( { cfg : input.cfg , model : input.model } )
2026-04-17 03:50:17 +00:00
const all = turns ( input . messages )
2026-05-12 20:33:16 +00:00
if ( ! all . length ) return { head : input.messages , tail_start_id : undefined }
2026-08-12 19:36:51 +00:00
const recent = limit === undefined ? all : all.slice ( - limit )
2026-04-17 03:50:17 +00:00
let total = 0
2026-04-22 15:07:32 +00:00
let keep : Tail | undefined
2026-04-17 03:50:17 +00:00
for ( let i = recent . length - 1 ; i >= 0 ; i -- ) {
2026-04-22 15:07:32 +00:00
const turn = recent [ i ] !
2026-08-12 19:36:51 +00:00
// estimate lazily so cost stays proportional to the retained tail, not the whole session
const size = yield * estimate ( {
messages : input.messages.slice ( turn . start , turn . end ) ,
model : input.model ,
} )
2026-04-22 15:07:32 +00:00
if ( total + size <= budget ) {
total += size
2026-05-12 20:33:16 +00:00
keep = { start : turn.start , id : turn.id }
2026-04-22 15:07:32 +00:00
continue
}
const remaining = budget - total
const split = yield * splitTurn ( {
messages : input.messages ,
turn ,
model : input.model ,
budget : remaining ,
estimate ,
} )
if ( split ) keep = split
2026-06-08 19:41:56 +00:00
else if ( ! keep ) {
yield * Effect . logInfo ( "tail fallback" , { budget , size , total } )
}
2026-04-22 15:07:32 +00:00
break
2026-04-17 03:50:17 +00:00
}
2026-05-12 20:33:16 +00:00
if ( ! keep || keep . start === 0 ) return { head : input.messages , tail_start_id : undefined }
2026-04-17 03:50:17 +00:00
return {
head : input.messages.slice ( 0 , keep . start ) ,
2026-05-12 20:33:16 +00:00
tail_start_id : keep.id ,
2026-04-17 03:50:17 +00:00
}
} )
2026-04-17 00:49:39 +00:00
// goes backwards through parts until there are PRUNE_PROTECT tokens worth of tool
// calls, then erases output of older tool calls to free context space
const prune = Effect . fn ( "SessionCompaction.prune" ) ( function * ( input : { sessionID : SessionID } ) {
const cfg = yield * config . get ( )
2026-04-19 23:34:44 +00:00
if ( ! cfg . compaction ? . prune ) return
2026-06-08 19:41:56 +00:00
yield * Effect . logInfo ( "pruning" )
2026-04-17 00:49:39 +00:00
const msgs = yield * session
. messages ( { sessionID : input.sessionID } )
. pipe ( Effect . catchIf ( NotFoundError . isInstance , ( ) = > Effect . succeed ( undefined ) ) )
if ( ! msgs ) return
let total = 0
let pruned = 0
2026-06-03 02:42:13 +00:00
const toPrune : SessionV1.ToolPart [ ] = [ ]
2026-04-17 00:49:39 +00:00
let turns = 0
loop : for ( let msgIndex = msgs . length - 1 ; msgIndex >= 0 ; msgIndex -- ) {
const msg = msgs [ msgIndex ]
if ( msg . info . role === "user" ) turns ++
if ( turns < 2 ) continue
if ( msg . info . role === "assistant" && msg . info . summary ) break loop
for ( let partIndex = msg . parts . length - 1 ; partIndex >= 0 ; partIndex -- ) {
const part = msg . parts [ partIndex ]
2026-04-22 15:07:32 +00:00
if ( part . type !== "tool" ) continue
if ( part . state . status !== "completed" ) continue
if ( PRUNE_PROTECTED_TOOLS . includes ( part . tool ) ) continue
if ( part . state . time . compacted ) break loop
const estimate = Token . estimate ( part . state . output )
total += estimate
if ( total <= PRUNE_PROTECT ) continue
pruned += estimate
toPrune . push ( part )
2026-04-17 00:49:39 +00:00
}
}
2026-06-08 19:41:56 +00:00
yield * Effect . logInfo ( "found" , { pruned , total } )
2026-04-17 00:49:39 +00:00
if ( pruned > PRUNE_MINIMUM ) {
for ( const part of toPrune ) {
if ( part . state . status === "completed" ) {
part . state . time . compacted = Date . now ( )
yield * session . updatePart ( part )
}
}
2026-06-08 19:41:56 +00:00
yield * Effect . logInfo ( "pruned" , { count : toPrune.length } )
2026-04-17 00:49:39 +00:00
}
} )
const processCompaction = Effect . fn ( "SessionCompaction.process" ) ( function * ( input : {
2026-03-28 01:09:14 +00:00
parentID : MessageID
2026-06-03 02:42:13 +00:00
messages : SessionV1.WithParts [ ]
2026-03-28 01:09:14 +00:00
sessionID : SessionID
auto : boolean
overflow? : boolean
2026-04-17 00:49:39 +00:00
} ) {
const parent = input . messages . findLast ( ( m ) = > m . info . id === input . parentID )
if ( ! parent || parent . info . role !== "user" ) {
throw new Error ( ` Compaction parent must be a user message: ${ input . parentID } ` )
}
const userMessage = parent . info
2026-06-03 02:43:28 +00:00
const compactionPart = parent . parts . find ( ( part ) : part is SessionV1 . CompactionPart = > part . type === "compaction" )
2026-04-17 00:49:39 +00:00
let messages = input . messages
let replay :
| {
2026-06-03 02:42:13 +00:00
info : SessionV1.User
parts : SessionV1.Part [ ]
2026-03-28 01:09:14 +00:00
}
2026-04-17 00:49:39 +00:00
| undefined
if ( input . overflow ) {
const idx = input . messages . findIndex ( ( m ) = > m . info . id === input . parentID )
for ( let i = idx - 1 ; i >= 0 ; i -- ) {
const msg = input . messages [ i ]
if ( msg . info . role === "user" && ! msg . parts . some ( ( p ) = > p . type === "compaction" ) ) {
replay = { info : msg.info , parts : msg.parts }
messages = input . messages . slice ( 0 , i )
break
2025-09-16 08:52:34 +00:00
}
}
2026-04-17 00:49:39 +00:00
const hasContent =
replay && messages . some ( ( m ) = > m . info . role === "user" && ! m . parts . some ( ( p ) = > p . type === "compaction" ) )
if ( ! hasContent ) {
replay = undefined
messages = input . messages
2026-03-02 07:40:55 +00:00
}
2026-04-17 00:49:39 +00:00
}
const agent = yield * agents . get ( "compaction" )
const model = agent . model
2026-05-13 22:23:09 +00:00
? yield * provider . getModel ( agent . model . providerID , agent . model . modelID ) . pipe ( Effect . orDie )
: yield * provider . getModel ( userMessage . model . providerID , userMessage . model . modelID ) . pipe ( Effect . orDie )
2026-04-17 03:50:17 +00:00
const cfg = yield * config . get ( )
const history = compactionPart && messages . at ( - 1 ) ? . info . id === input . parentID ? messages . slice ( 0 , - 1 ) : messages
2026-04-22 15:07:32 +00:00
const prior = completedCompactions ( history )
const hidden = new Set ( prior . flatMap ( ( item ) = > [ item . userIndex , item . assistantIndex ] ) )
const previousSummary = prior . at ( - 1 ) ? . summary
2026-04-17 03:50:17 +00:00
const selected = yield * select ( {
2026-04-22 15:07:32 +00:00
messages : history.filter ( ( _ , index ) = > ! hidden . has ( index ) ) ,
2026-04-17 03:50:17 +00:00
cfg ,
model ,
} )
2026-04-17 00:49:39 +00:00
// Allow plugins to inject context or replace compaction prompt.
const compacting = yield * plugin . trigger (
"experimental.session.compacting" ,
{ sessionID : input.sessionID } ,
{ context : [ ] , prompt : undefined } ,
)
2026-04-17 03:50:17 +00:00
const msgs = structuredClone ( selected . head )
2026-04-17 00:49:39 +00:00
yield * plugin . trigger ( "experimental.chat.messages.transform" , { } , { messages : msgs } )
2026-08-07 02:11:14 +00:00
const conversation = msgs . map ( serialize ) . filter ( Boolean ) . join ( "\n\n" )
2026-08-12 19:36:51 +00:00
const nextPrompt =
compacting . prompt ? ?
[
buildPrompt ( {
previousSummary ,
context : [ conversation ] ,
} ) ,
. . . compacting . context ,
]
. filter ( Boolean )
. join ( "\n\n" )
2026-04-17 00:49:39 +00:00
const ctx = yield * InstanceState . context
2026-06-03 02:42:13 +00:00
const msg : SessionV1.Assistant = {
2026-04-17 00:49:39 +00:00
id : MessageID.ascending ( ) ,
role : "assistant" ,
parentID : input.parentID ,
sessionID : input.sessionID ,
mode : "compaction" ,
agent : "compaction" ,
variant : userMessage.model.variant ,
summary : true ,
path : {
cwd : ctx.directory ,
root : ctx.worktree ,
} ,
cost : 0 ,
tokens : {
output : 0 ,
input : 0 ,
reasoning : 0 ,
cache : { read : 0 , write : 0 } ,
} ,
modelID : model.id ,
providerID : model.providerID ,
time : {
created : Date.now ( ) ,
} ,
}
yield * session . updateMessage ( msg )
const processor = yield * processors . create ( {
assistantMessage : msg ,
sessionID : input.sessionID ,
model ,
} )
const result = yield * processor . process ( {
user : userMessage ,
agent ,
sessionID : input.sessionID ,
tools : { } ,
system : [ ] ,
messages : [
{
role : "user" ,
2026-08-07 02:11:14 +00:00
content : [
{
type : "text" ,
2026-08-12 19:36:51 +00:00
text : [
nextPrompt ,
. . . ( compacting . prompt ? [ "The following is the conversation history:" , conversation ] : [ ] ) ,
]
2026-08-07 02:11:14 +00:00
. filter ( Boolean )
. join ( "\n\n" ) ,
} ,
] ,
2026-03-28 16:09:47 +00:00
} ,
2026-04-17 00:49:39 +00:00
] ,
model ,
} )
2026-03-28 01:09:14 +00:00
2026-04-17 00:49:39 +00:00
if ( result === "compact" ) {
2026-06-03 02:42:13 +00:00
processor . message . error = new SessionV1 . ContextOverflowError ( {
2026-04-17 00:49:39 +00:00
message : replay
? "Conversation history too large to compact - exceeds model context limit"
: "Session too large to compact - context exceeds model limit even after stripping media" ,
} ) . toObject ( )
processor . message . finish = "error"
yield * session . updateMessage ( processor . message )
return "stop"
}
2026-05-12 20:33:16 +00:00
if ( compactionPart && selected . tail_start_id && compactionPart . tail_start_id !== selected . tail_start_id ) {
yield * session . updatePart ( {
. . . compactionPart ,
tail_start_id : selected.tail_start_id ,
} )
}
2026-04-17 00:49:39 +00:00
if ( result === "continue" && input . auto ) {
if ( replay ) {
const original = replay . info
const replayMsg = yield * session . updateMessage ( {
id : MessageID.ascending ( ) ,
role : "user" ,
sessionID : input.sessionID ,
time : { created : Date.now ( ) } ,
agent : original.agent ,
model : original.model ,
format : original.format ,
tools : original.tools ,
system : original.system ,
} )
for ( const part of replay . parts ) {
if ( part . type === "compaction" ) continue
const replayPart =
part . type === "file" && MessageV2 . isMedia ( part . mime )
? { type : "text" as const , text : ` [Attached ${ part . mime } : ${ part . filename ? ? "file" } ] ` }
: part
yield * session . updatePart ( {
. . . replayPart ,
id : PartID.ascending ( ) ,
messageID : replayMsg.id ,
2026-03-28 01:09:14 +00:00
sessionID : input.sessionID ,
} )
}
2026-04-17 00:49:39 +00:00
}
2026-03-28 01:09:14 +00:00
2026-04-17 00:49:39 +00:00
if ( ! replay ) {
const info = yield * provider . getProvider ( userMessage . model . providerID )
if (
( yield * plugin . trigger (
"experimental.compaction.autocontinue" ,
{
2026-04-14 01:14:53 +00:00
sessionID : input.sessionID ,
agent : userMessage.agent ,
2026-05-13 22:23:09 +00:00
model : yield * provider
. getModel ( userMessage . model . providerID , userMessage . model . modelID )
. pipe ( Effect . orDie ) ,
2026-04-17 00:49:39 +00:00
provider : {
source : info.source ,
info ,
options : info.options ,
2026-04-14 01:14:53 +00:00
} ,
2026-04-17 00:49:39 +00:00
message : userMessage ,
overflow : input.overflow === true ,
} ,
{ enabled : true } ,
) ) . enabled
) {
const continueMsg = yield * session . updateMessage ( {
id : MessageID.ascending ( ) ,
role : "user" ,
sessionID : input.sessionID ,
time : { created : Date.now ( ) } ,
agent : userMessage.agent ,
model : userMessage.model ,
} )
const text =
( input . overflow
? "The previous request exceeded the provider's size limit due to large media attachments. The conversation was compacted and media files were removed from context. If the user was asking about attached images or files, explain that the attachments were too large to process and suggest they try again with smaller or fewer files.\n\n"
: "" ) +
"Continue if you have next steps, or stop and ask for clarification if you are unsure how to proceed."
yield * session . updatePart ( {
id : PartID.ascending ( ) ,
messageID : continueMsg.id ,
sessionID : input.sessionID ,
type : "text" ,
// Internal marker for auto-compaction followups so provider plugins
// can distinguish them from manual post-compaction user prompts.
// This is not a stable plugin contract and may change or disappear.
metadata : { compaction_continue : true } ,
synthetic : true ,
text ,
time : {
start : Date.now ( ) ,
end : Date.now ( ) ,
} ,
} )
2026-03-28 01:09:14 +00:00
}
2026-03-02 07:40:55 +00:00
}
2026-04-17 00:49:39 +00:00
}
2026-03-28 01:09:14 +00:00
2026-04-17 00:49:39 +00:00
if ( processor . message . error ) return "stop"
2026-05-03 02:09:48 +00:00
if ( result === "continue" ) {
2026-05-31 01:08:38 +00:00
yield * events . publish ( Event . Compacted , { sessionID : input.sessionID } )
2026-05-03 02:09:48 +00:00
}
2026-04-17 00:49:39 +00:00
return result
} )
2026-03-28 01:09:14 +00:00
2026-04-17 00:49:39 +00:00
const create = Effect . fn ( "SessionCompaction.create" ) ( function * ( input : {
sessionID : SessionID
agent : string
2026-06-04 06:57:43 +00:00
model : { providerID : ProviderV2.ID ; modelID : ModelV2.ID }
2026-04-17 00:49:39 +00:00
auto : boolean
overflow? : boolean
} ) {
const msg = yield * session . updateMessage ( {
id : MessageID.ascending ( ) ,
role : "user" ,
model : input.model ,
sessionID : input.sessionID ,
agent : input.agent ,
time : { created : Date.now ( ) } ,
2026-03-28 01:09:14 +00:00
} )
2026-04-17 00:49:39 +00:00
yield * session . updatePart ( {
id : PartID.ascending ( ) ,
messageID : msg.id ,
sessionID : msg.sessionID ,
type : "compaction" ,
auto : input.auto ,
overflow : input.overflow ,
2026-03-28 01:09:14 +00:00
} )
2026-04-17 00:49:39 +00:00
} )
return Service . of ( {
isOverflow ,
prune ,
process : processCompaction ,
create ,
} )
} ) ,
)
2026-06-25 18:34:17 +00:00
export const node = LayerNode . make ( {
service : Service ,
layer : layer ,
deps : [
Config . node ,
Session . node ,
Agent . node ,
Plugin . node ,
SessionProcessor . node ,
Provider . node ,
EventV2Bridge . node ,
RuntimeFlags . node ,
] ,
} )
2026-06-09 19:31:31 +00:00
2026-04-17 00:49:39 +00:00
export * as SessionCompaction from "./compaction"