@@ -14,11 +14,55 @@ import { Agent } from "@/agent/agent"
1414import { Plugin } from "@/plugin"
1515import { Config } from "@/config/config"
1616import { ProviderTransform } from "@/provider/transform"
17+ import { Telemetry } from "@/telemetry"
1718import { ModelID , ProviderID } from "@/provider/schema"
1819
1920export namespace SessionCompaction {
2021 const log = Log . create ( { service : "session.compaction" } )
2122
23+ // altimate_change: observation masks for pruned tool outputs
24+ function formatBytes ( bytes : number ) : string {
25+ if ( bytes < 1024 ) return `${ bytes } B`
26+ if ( bytes < 1024 * 1024 ) return `${ ( bytes / 1024 ) . toFixed ( 1 ) } KB`
27+ return `${ ( bytes / ( 1024 * 1024 ) ) . toFixed ( 1 ) } MB`
28+ }
29+
30+ function truncateArgs ( input : Record < string , any > | null | undefined , maxLen : number ) : string {
31+ if ( ! input || typeof input !== "object" ) return ""
32+ let str : string
33+ try {
34+ str = Object . entries ( input )
35+ . map ( ( [ k , v ] ) => `${ k } : ${ JSON . stringify ( v ) } ` )
36+ . join ( ", " )
37+ } catch {
38+ return "[unserializable]"
39+ }
40+ if ( str . length <= maxLen ) return str
41+ let end = maxLen
42+ const code = str . charCodeAt ( end - 1 )
43+ if ( code >= 0xd800 && code <= 0xdbff ) end --
44+ return str . slice ( 0 , end ) + "…"
45+ }
46+
47+ export function createObservationMask ( part : MessageV2 . ToolPart ) : string {
48+ const output =
49+ ( part . state . status === "completed" ? part . state . output : "" ) || ""
50+ const lines = output . split ( "\n" ) . length
51+ const bytes = Buffer . byteLength ( output , "utf8" )
52+ const args = truncateArgs (
53+ part . state . status === "completed" ||
54+ part . state . status === "running" ||
55+ part . state . status === "error"
56+ ? part . state . input
57+ : { } ,
58+ 80 ,
59+ )
60+ const firstLine = output . split ( "\n" ) [ 0 ] ?. slice ( 0 , 80 ) || ""
61+ const fingerprint = firstLine ? ` — "${ firstLine } "` : ""
62+ return `[Tool output cleared — ${ part . tool } (${ args } ) returned ${ lines } lines, ${ formatBytes ( bytes ) } ${ fingerprint } ]`
63+ }
64+ // end altimate_change
65+
2266 export const Event = {
2367 Compacted : BusEvent . define (
2468 "session.compacted" ,
@@ -30,6 +74,8 @@ export namespace SessionCompaction {
3074
3175 const COMPACTION_BUFFER = 20_000
3276
77+ // altimate_change: improved isOverflow formula with safety guard and unified headroom
78+ // See PR #35 — fixes upstream bugs with limit.input models and small-context models
3379 export async function isOverflow ( input : { tokens : MessageV2 . Assistant [ "tokens" ] ; model : Provider . Model } ) {
3480 const config = await Config . get ( )
3581 if ( config . compaction ?. auto === false ) return false
@@ -40,13 +86,14 @@ export namespace SessionCompaction {
4086 input . tokens . total ||
4187 input . tokens . input + input . tokens . output + input . tokens . cache . read + input . tokens . cache . write
4288
43- const reserved =
44- config . compaction ?. reserved ?? Math . min ( COMPACTION_BUFFER , ProviderTransform . maxOutputTokens ( input . model ) )
45- const usable = input . model . limit . input
46- ? input . model . limit . input - reserved
47- : context - ProviderTransform . maxOutputTokens ( input . model )
48- return count >= usable
89+ const maxOutput = ProviderTransform . maxOutputTokens ( input . model )
90+ const reserved = config . compaction ?. reserved ?? COMPACTION_BUFFER
91+ const headroom = Math . max ( reserved , maxOutput )
92+ const base = input . model . limit . input ?? context
93+ if ( base <= headroom ) return false
94+ return count >= base - headroom
4995 }
96+ // end altimate_change
5097
5198 export const PRUNE_MINIMUM = 20_000
5299 export const PRUNE_PROTECT = 40_000
@@ -91,14 +138,34 @@ export namespace SessionCompaction {
91138 if ( pruned > PRUNE_MINIMUM ) {
92139 for ( const part of toPrune ) {
93140 if ( part . state . status === "completed" ) {
141+ // altimate_change: observation masks for pruned tool outputs
142+ const mask = createObservationMask ( part )
94143 part . state . time . compacted = Date . now ( )
144+ part . state . metadata = {
145+ ...part . state . metadata ,
146+ observation_mask : mask ,
147+ }
148+ // end altimate_change
95149 await Session . updatePart ( part )
96150 }
97151 }
98152 log . info ( "pruned" , { count : toPrune . length } )
153+ // altimate_change: telemetry for pruning
154+ Telemetry . track ( {
155+ type : "tool_outputs_pruned" ,
156+ timestamp : Date . now ( ) ,
157+ session_id : input . sessionID ,
158+ count : toPrune . length ,
159+ tokens_pruned : pruned ,
160+ } )
161+ // end altimate_change
99162 }
100163 }
101164
165+ // altimate_change: compaction attempt tracking for loop protection
166+ const compactionAttempts = new Map < string , number > ( )
167+ // end altimate_change
168+
102169 export async function process ( input : {
103170 parentID : MessageID
104171 messages : MessageV2 . WithParts [ ]
@@ -107,6 +174,20 @@ export namespace SessionCompaction {
107174 auto : boolean
108175 overflow ?: boolean
109176 } ) {
177+ // altimate_change: telemetry and attempt tracking
178+ const attempt = ( compactionAttempts . get ( input . sessionID ) ?? 0 ) + 1
179+ compactionAttempts . set ( input . sessionID , attempt )
180+ input . abort . addEventListener ( "abort" , ( ) => {
181+ compactionAttempts . delete ( input . sessionID )
182+ } , { once : true } )
183+ Telemetry . track ( {
184+ type : "compaction_triggered" ,
185+ timestamp : Date . now ( ) ,
186+ session_id : input . sessionID ,
187+ trigger : input . auto ? "overflow_detection" : "error_recovery" ,
188+ attempt,
189+ } )
190+ // end altimate_change
110191 const userMessage = input . messages . findLast ( ( m ) => m . info . id === input . parentID ) ! . info as MessageV2 . User
111192
112193 let messages = input . messages
@@ -186,6 +267,15 @@ When constructing the summary, try to stick to this template:
186267- [What important instructions did the user give you that are relevant]
187268- [If there is a plan or spec, include information about it so next agent can continue using it]
188269
270+ ## Data Context
271+
272+ - [What warehouse(s) or database(s) are we connected to?]
273+ - [What schemas, tables, or columns were discovered or are relevant?]
274+ - [What dbt models, sources, or tests are involved?]
275+ - [Any lineage findings (upstream/downstream dependencies)?]
276+ - [Any query patterns, anti-patterns, or optimization opportunities found?]
277+ - [Skip this section entirely if the task is not data-engineering related]
278+
189279## Discoveries
190280
191281[What notable things were learned during this conversation that would be useful for the next agent to know when continuing the work]
@@ -289,8 +379,12 @@ When constructing the summary, try to stick to this template:
289379 } )
290380 }
291381 }
292- if ( processor . message . error ) return "stop"
382+ if ( processor . message . error ) {
383+ compactionAttempts . delete ( input . sessionID ) // altimate_change
384+ return "stop"
385+ }
293386 Bus . publish ( Event . Compacted , { sessionID : input . sessionID } )
387+ compactionAttempts . delete ( input . sessionID ) // altimate_change
294388 return "continue"
295389 }
296390
0 commit comments