2025-12-09 19:32:04 +00:00
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
2026-04-16 03:41:34 +00:00
import * as 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"
import * as Log from "@opencode-ai/core/util/log"
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-03-12 13:27:52 +00:00
import { ModelID , ProviderID } from "@/provider/schema"
2026-04-23 19:37:44 +00:00
import { Effect , Layer , Context , Schema } from "effect"
2026-05-03 02:09:48 +00:00
import * as DateTime from "effect/DateTime"
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"
import { SessionEvent } from "@opencode-ai/core/session-event"
2025-09-13 09:46:14 +00:00
2026-04-17 00:49:39 +00:00
const log = Log . create ( { service : "session.compaction" } )
export const Event = {
Compacted : BusEvent.define (
"session.compacted" ,
2026-04-23 19:37:44 +00:00
Schema . Struct ( {
sessionID : SessionID ,
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-17 03:50:17 +00:00
const DEFAULT_TAIL_TURNS = 2
2026-04-20 06:06:29 +00:00
const MIN_PRESERVE_RECENT_TOKENS = 2 _000
const MAX_PRESERVE_RECENT_TOKENS = 8 _000
2026-04-27 22:15:11 +00:00
const SUMMARY_TEMPLATE = ` Output exactly the Markdown structure shown inside <template> and keep the section order unchanged. Do not include the <template> tags in your response.
< template >
2026-04-22 15:07:32 +00:00
# # Goal
- [ single - sentence task summary ]
# # Constraints & Preferences
- [ user constraints , preferences , specs , or "(none)" ]
# # Progress
# # # Done
- [ completed work or "(none)" ]
# # # In Progress
- [ current work or "(none)" ]
# # # Blocked
- [ blockers or "(none)" ]
# # Key Decisions
- [ decision and why , or "(none)" ]
# # Next Steps
- [ ordered next actions or "(none)" ]
# # Critical Context
- [ important technical facts , errors , open questions , or "(none)" ]
# # Relevant Files
- [ file or directory path : why it matters , or "(none)" ]
2026-04-27 22:15:11 +00:00
< / template >
2026-04-22 15:07:32 +00:00
Rules :
- Keep every section , even when empty .
- Use terse bullets , not prose paragraphs .
- Preserve exact file paths , commands , error strings , and identifiers when known .
- Do not mention the summary process or that context was compacted . `
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
}
function summaryText ( message : MessageV2.WithParts ) {
const text = message . parts
. filter ( ( part ) : part is MessageV2 . TextPart = > part . type === "text" )
. map ( ( part ) = > part . text . trim ( ) )
. filter ( Boolean )
. join ( "\n\n" )
. trim ( )
return text || undefined
}
function completedCompactions ( messages : MessageV2.WithParts [ ] ) {
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-05-12 20:33:16 +00:00
function buildPrompt ( input : { previousSummary? : string ; context : string [ ] } ) {
2026-04-22 15:07:32 +00:00
const anchor = input . previousSummary
? [
2026-05-12 20:33:16 +00:00
"Update the anchored summary below using the conversation history above." ,
2026-04-22 15:07:32 +00:00
"Preserve still-true details, remove stale details, and merge in the new facts." ,
"<previous-summary>" ,
input . previousSummary ,
"</previous-summary>" ,
] . join ( "\n" )
2026-05-12 20:33:16 +00:00
: "Create a new anchored summary from the conversation history above."
return [ anchor , SUMMARY_TEMPLATE , . . . input . context ] . join ( "\n\n" )
2026-04-22 15:07:32 +00:00
}
2026-04-20 06:06:29 +00:00
function preserveRecentBudget ( input : { cfg : Config.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-04-17 03:50:17 +00:00
function turns ( messages : MessageV2.WithParts [ ] ) {
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 : {
messages : MessageV2.WithParts [ ]
turn : Turn
model : Provider.Model
budget : number
estimate : ( input : { messages : MessageV2.WithParts [ ] ; model : Provider.Model } ) = > Effect . Effect < number >
} ) {
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 : {
tokens : MessageV2.Assistant [ "tokens" ]
model : Provider.Model
} ) = > Effect . Effect < boolean >
readonly prune : ( input : { sessionID : SessionID } ) = > Effect . Effect < void >
readonly process : ( input : {
parentID : MessageID
messages : MessageV2.WithParts [ ]
sessionID : SessionID
auto : boolean
overflow? : boolean
} ) = > Effect . Effect < "continue" | "stop" >
readonly create : ( input : {
sessionID : SessionID
agent : string
model : { providerID : ProviderID ; modelID : ModelID }
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-05-15 00:50:23 +00:00
export const layer = Layer . effect (
2026-04-17 00:49:39 +00:00
Service ,
Effect . gen ( function * ( ) {
const bus = yield * Bus . Service
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-03-28 01:09:14 +00:00
tokens : MessageV2.Assistant [ "tokens" ]
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 : {
messages : MessageV2.WithParts [ ]
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 : {
messages : MessageV2.WithParts [ ]
cfg : Config.Info
model : Provider.Model
} ) {
const limit = input . cfg . compaction ? . tail_turns ? ? DEFAULT_TAIL_TURNS
2026-05-12 20:33:16 +00:00
if ( 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-04-17 03:50:17 +00:00
const recent = all . slice ( - limit )
const sizes = yield * Effect . forEach (
recent ,
( turn ) = >
estimate ( {
messages : input.messages.slice ( turn . start , turn . end ) ,
model : input.model ,
} ) ,
{ concurrency : 1 } ,
)
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-04-17 03:50:17 +00:00
const size = sizes [ i ]
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
else if ( ! keep ) log . info ( "tail fallback" , { budget , size , total } )
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-04-17 00:49:39 +00:00
log . info ( "pruning" )
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
const toPrune : MessageV2.ToolPart [ ] = [ ]
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
}
}
log . info ( "found" , { pruned , total } )
if ( pruned > PRUNE_MINIMUM ) {
for ( const part of toPrune ) {
if ( part . state . status === "completed" ) {
part . state . time . compacted = Date . now ( )
yield * session . updatePart ( part )
}
}
log . info ( "pruned" , { count : toPrune.length } )
}
} )
const processCompaction = Effect . fn ( "SessionCompaction.process" ) ( function * ( input : {
2026-03-28 01:09:14 +00:00
parentID : MessageID
messages : MessageV2.WithParts [ ]
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-04-17 03:50:17 +00:00
const compactionPart = parent . parts . find ( ( part ) : part is MessageV2 . CompactionPart = > part . type === "compaction" )
2026-04-17 00:49:39 +00:00
let messages = input . messages
let replay :
| {
info : MessageV2.User
parts : MessageV2.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-05-12 20:33:16 +00:00
const nextPrompt = compacting . prompt ? ? buildPrompt ( { previousSummary , context : compacting.context } )
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-04-22 15:07:32 +00:00
const modelMessages = yield * MessageV2 . toModelMessagesEffect ( msgs , model , {
stripMedia : true ,
toolOutputMaxChars : TOOL_OUTPUT_MAX_CHARS ,
} )
2026-04-17 00:49:39 +00:00
const ctx = yield * InstanceState . context
const msg : MessageV2.Assistant = {
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 : [
. . . modelMessages ,
{
role : "user" ,
2026-04-22 15:07:32 +00:00
content : [ { type : "text" , text : nextPrompt } ] ,
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" ) {
processor . message . error = new MessageV2 . ContextOverflowError ( {
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" ) {
const summary = summaryText (
2026-05-13 07:52:12 +00:00
( yield * session . messages ( { sessionID : input.sessionID } ) . pipe ( Effect . orDie ) ) . find (
( item ) = > item . info . id === msg . id ,
) ? ? {
2026-05-03 02:09:48 +00:00
info : msg ,
parts : [ ] ,
} ,
)
2026-05-13 12:14:09 +00:00
if ( flags . experimentalEventSystem ) {
2026-05-15 00:50:23 +00:00
yield * events . publish ( SessionEvent . Compaction . Ended , {
2026-05-11 01:20:13 +00:00
sessionID : input.sessionID ,
timestamp : DateTime.makeUnsafe ( Date . now ( ) ) ,
text : summary ? ? "" ,
2026-05-12 20:33:16 +00:00
include : selected.tail_start_id ,
2026-05-11 01:20:13 +00:00
} )
}
2026-05-03 02:09:48 +00:00
yield * bus . publish ( Event . Compacted , { sessionID : input.sessionID } )
}
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
model : { providerID : ProviderID ; modelID : ModelID }
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-05-13 12:14:09 +00:00
if ( flags . experimentalEventSystem ) {
2026-05-15 00:50:23 +00:00
yield * events . publish ( SessionEvent . Compaction . Started , {
2026-05-11 01:20:13 +00:00
sessionID : input.sessionID ,
timestamp : DateTime.makeUnsafe ( Date . now ( ) ) ,
reason : input.auto ? "auto" : "manual" ,
} )
}
2026-04-17 00:49:39 +00:00
} )
return Service . of ( {
isOverflow ,
prune ,
process : processCompaction ,
create ,
} )
} ) ,
)
export const defaultLayer = Layer . suspend ( ( ) = >
layer . pipe (
Layer . provide ( Provider . defaultLayer ) ,
Layer . provide ( Session . defaultLayer ) ,
Layer . provide ( SessionProcessor . defaultLayer ) ,
Layer . provide ( Agent . defaultLayer ) ,
Layer . provide ( Plugin . defaultLayer ) ,
Layer . provide ( Bus . layer ) ,
Layer . provide ( Config . defaultLayer ) ,
2026-05-13 12:14:09 +00:00
Layer . provide ( RuntimeFlags . defaultLayer ) ,
2026-05-15 00:50:23 +00:00
Layer . provide ( EventV2Bridge . defaultLayer ) ,
2026-04-17 00:49:39 +00:00
) ,
)
export * as SessionCompaction from "./compaction"