@@ -68,10 +68,104 @@ export class Logger {
6868 return this . write ( "ERROR" , component , message , data )
6969 }
7070
71+ /**
72+ * Parses janitor prompt to extract structured components
73+ * Returns null if parsing fails (not a janitor prompt or malformed)
74+ *
75+ * Note: The session history in the prompt has literal newlines (not \n escapes)
76+ * due to prompt.ts line 93 doing .replace(/\\n/g, '\n') for readability.
77+ * We need to reverse this before parsing.
78+ */
79+ private parseJanitorPrompt ( prompt : string ) : {
80+ instructions : string
81+ availableToolCallIds : string [ ]
82+ sessionHistory : any [ ]
83+ responseSchema : any
84+ } | null {
85+ try {
86+ // Extract available tool call IDs
87+ const idsMatch = prompt . match ( / A v a i l a b l e t o o l c a l l I D s f o r a n a l y s i s : \s * ( [ ^ \n ] + ) / )
88+ const availableToolCallIds = idsMatch
89+ ? idsMatch [ 1 ] . split ( ',' ) . map ( id => id . trim ( ) )
90+ : [ ]
91+
92+ // Extract session history (between "Session history:\n" and "\n\nYou MUST respond")
93+ // The captured text has literal newlines, so we need to escape them back to \n for valid JSON
94+ const historyMatch = prompt . match ( / S e s s i o n h i s t o r y : \s * \n ( [ \s \S ] * ?) \n \n Y o u M U S T r e s p o n d / )
95+ let sessionHistory : any [ ] = [ ]
96+
97+ if ( historyMatch ) {
98+ // Re-escape newlines in string literals for valid JSON parsing
99+ // This reverses the .replace(/\\n/g, '\n') done in prompt.ts
100+ const historyText = historyMatch [ 1 ]
101+
102+ // Fix: escape literal newlines within strings to make valid JSON
103+ // We need to be careful to only escape newlines inside string values
104+ const fixedJson = this . escapeNewlinesInJson ( historyText )
105+ sessionHistory = JSON . parse ( fixedJson )
106+ }
107+
108+ // Extract instructions (everything before "IMPORTANT: Available tool call IDs")
109+ const instructionsMatch = prompt . match ( / ( [ \s \S ] * ?) \n \n I M P O R T A N T : A v a i l a b l e t o o l c a l l I D s / )
110+ const instructions = instructionsMatch
111+ ? instructionsMatch [ 1 ] . trim ( )
112+ : ''
113+
114+ // Extract response schema (after "You MUST respond with valid JSON matching this exact schema:")
115+ // Note: The schema contains "..." placeholders which aren't valid JSON, so we save it as a string
116+ const schemaMatch = prompt . match ( / m a t c h i n g t h i s e x a c t s c h e m a : \s * \n ( \{ [ \s \S ] * ?\} ) \s * \n \n R e t u r n O N L Y / )
117+ const responseSchema = schemaMatch
118+ ? schemaMatch [ 1 ] // Keep as string since it has "..." placeholders
119+ : null
120+
121+ return {
122+ instructions,
123+ availableToolCallIds,
124+ sessionHistory,
125+ responseSchema
126+ }
127+ } catch ( error ) {
128+ // If parsing fails, return null and fall back to default logging
129+ return null
130+ }
131+ }
132+
133+ /**
134+ * Helper to escape literal newlines within JSON string values
135+ * This makes JSON with literal newlines parseable again
136+ */
137+ private escapeNewlinesInJson ( jsonText : string ) : string {
138+ // Strategy: Replace literal newlines that appear inside strings with \\n
139+ // We detect being "inside a string" by tracking quotes
140+ let result = ''
141+ let inString = false
142+ let escaped = false
143+
144+ for ( let i = 0 ; i < jsonText . length ; i ++ ) {
145+ const char = jsonText [ i ]
146+ const prevChar = i > 0 ? jsonText [ i - 1 ] : ''
147+
148+ if ( char === '"' && prevChar !== '\\' ) {
149+ inString = ! inString
150+ result += char
151+ } else if ( char === '\n' && inString ) {
152+ // Replace literal newline with escaped version
153+ result += '\\n'
154+ } else {
155+ result += char
156+ }
157+ }
158+
159+ return result
160+ }
161+
71162 /**
72163 * Saves AI context to a dedicated directory for debugging
73164 * Each call creates a new timestamped file in ~/.config/opencode/logs/dcp/ai-context/
74165 * Only writes if debug is enabled
166+ *
167+ * For janitor-shadow sessions, parses and structures the embedded session history
168+ * for better readability
75169 */
76170 async saveWrappedContext ( sessionID : string , messages : any [ ] , metadata : any ) {
77171 if ( ! this . enabled ) return
@@ -90,20 +184,66 @@ export class Logger {
90184 const filename = `${ timestamp } _${ counter } _${ sessionID . substring ( 0 , 15 ) } .json`
91185 const filepath = join ( aiContextDir , filename )
92186
93- const content = {
94- timestamp : new Date ( ) . toISOString ( ) ,
95- sessionID,
96- metadata,
97- messages
187+ // Check if this is a janitor-shadow session
188+ const isJanitorShadow = sessionID === "janitor-shadow" &&
189+ messages . length === 1 &&
190+ messages [ 0 ] ?. role === 'user' &&
191+ typeof messages [ 0 ] ?. content === 'string'
192+
193+ let content : any
194+
195+ if ( isJanitorShadow ) {
196+ // Parse the janitor prompt to extract structured data
197+ const parsed = this . parseJanitorPrompt ( messages [ 0 ] . content )
198+
199+ if ( parsed ) {
200+ // Create enhanced structured format for readability
201+ content = {
202+ timestamp : new Date ( ) . toISOString ( ) ,
203+ sessionID,
204+ metadata,
205+ janitorAnalysis : {
206+ instructions : parsed . instructions ,
207+ availableToolCallIds : parsed . availableToolCallIds ,
208+ protectedTools : [ "task" , "todowrite" , "todoread" ] , // From prompt
209+ sessionHistory : parsed . sessionHistory ,
210+ responseSchema : parsed . responseSchema
211+ } ,
212+ // Keep raw prompt for reference/debugging
213+ rawPrompt : messages [ 0 ] . content
214+ }
215+ } else {
216+ // Parsing failed, use default format
217+ content = {
218+ timestamp : new Date ( ) . toISOString ( ) ,
219+ sessionID,
220+ metadata,
221+ messages,
222+ note : "Failed to parse janitor prompt structure"
223+ }
224+ }
225+ } else {
226+ // Standard format for non-janitor sessions
227+ content = {
228+ timestamp : new Date ( ) . toISOString ( ) ,
229+ sessionID,
230+ metadata,
231+ messages
232+ }
98233 }
99234
100- await writeFile ( filepath , JSON . stringify ( content , null , 2 ) )
235+ // Pretty print with 2-space indentation
236+ const jsonString = JSON . stringify ( content , null , 2 )
237+
238+ await writeFile ( filepath , jsonString )
101239
102240 // Log that we saved it
103241 await this . debug ( "logger" , "Saved AI context" , {
104242 sessionID,
105243 filepath,
106- messageCount : messages . length
244+ messageCount : messages . length ,
245+ isJanitorShadow,
246+ parsed : isJanitorShadow
107247 } )
108248 } catch ( error ) {
109249 // Silently fail - don't break the plugin if logging fails
0 commit comments