Skip to content

Commit bed94ae

Browse files
committed
Parse and structure janitor-shadow session logs for better readability
1 parent 003b25d commit bed94ae

File tree

1 file changed

+147
-7
lines changed

1 file changed

+147
-7
lines changed

lib/logger.ts

Lines changed: 147 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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(/Available tool call IDs for analysis:\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(/Session history:\s*\n([\s\S]*?)\n\nYou MUST respond/)
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\nIMPORTANT: Available tool call IDs/)
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(/matching this exact schema:\s*\n(\{[\s\S]*?\})\s*\n\nReturn ONLY/)
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

Comments
 (0)