From 953d8ebc63295954d783f44ba38c928e83396711 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Mon, 22 Dec 2025 02:44:00 -0500 Subject: [PATCH 1/2] feat: add debug context logging for session messages - Add saveContext method to Logger that saves transformed messages as JSON - Save to ~/.config/opencode/logs/dcp/context/{sessionId}/{timestamp}.json - Only logs when debug.enabled is true in config - Minimize output by stripping unnecessary metadata (IDs, summary, path, etc.) - Keep essential fields: role, time, tokens, text/tool parts --- lib/hooks.ts | 4 +++ lib/logger.ts | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/lib/hooks.ts b/lib/hooks.ts index b9ef006a..be9e851a 100644 --- a/lib/hooks.ts +++ b/lib/hooks.ts @@ -28,6 +28,10 @@ export function createChatMessageTransformHandler( prune(state, logger, config, output.messages) insertPruneToolContext(state, config, logger, output.messages) + + if (state.sessionId) { + await logger.saveContext(state.sessionId, output.messages) + } } } diff --git a/lib/logger.ts b/lib/logger.ts index d101c673..c86a53dc 100644 --- a/lib/logger.ts +++ b/lib/logger.ts @@ -107,4 +107,99 @@ export class Logger { const component = this.getCallerFile(2) return this.write("ERROR", component, message, data) } + + /** + * Strips unnecessary metadata from messages for cleaner debug logs. + * + * Removed: + * - All IDs (id, sessionID, messageID, parentID, callID on parts) + * - summary, path, cost, model, agent, mode, finish, providerID, modelID + * - step-start and step-finish parts entirely + * - snapshot fields + * - ignored text parts + * + * Kept: + * - role, time (created only), tokens (input, output, reasoning, cache) + * - text, reasoning, tool parts with content + * - tool calls with: tool, callID, input, output + */ + private minimizeForDebug(messages: any[]): any[] { + return messages.map((msg) => { + const minimized: any = { + role: msg.info?.role, + } + + if (msg.info?.time?.created) { + minimized.time = msg.info.time.created + } + + if (msg.info?.tokens) { + minimized.tokens = { + input: msg.info.tokens.input, + output: msg.info.tokens.output, + reasoning: msg.info.tokens.reasoning, + cache: msg.info.tokens.cache, + } + } + + if (msg.parts) { + minimized.parts = msg.parts + .map((part: any) => { + if (part.type === "step-start" || part.type === "step-finish") { + return null + } + + if (part.type === "text") { + if (part.ignored) return null + return { type: "text", text: part.text } + } + + if (part.type === "reasoning") { + return { + type: "reasoning", + text: part.text, + } + } + + if (part.type === "tool") { + const toolPart: any = { + type: "tool", + tool: part.tool, + callID: part.callID, + } + + if (part.state?.input) { + toolPart.input = part.state.input + } + if (part.state?.output) { + toolPart.output = part.state.output + } + + return toolPart + } + + return null + }) + .filter(Boolean) + } + + return minimized + }) + } + + async saveContext(sessionId: string, messages: any[]) { + if (!this.enabled) return + + try { + const contextDir = join(this.logDir, "context", sessionId) + if (!existsSync(contextDir)) { + await mkdir(contextDir, { recursive: true }) + } + + const minimized = this.minimizeForDebug(messages) + const timestamp = new Date().toISOString().replace(/[:.]/g, "-") + const contextFile = join(contextDir, `${timestamp}.json`) + await writeFile(contextFile, JSON.stringify(minimized, null, 2)) + } catch (error) {} + } } From 481aef970da7ebfde283c131907cd0a79820f349 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Mon, 22 Dec 2025 02:49:01 -0500 Subject: [PATCH 2/2] chore: add SCHEMA_NOTES.md to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 28a0e829..9f567cb5 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ test-update.ts # Documentation (local development only) docs/ +SCHEMA_NOTES.md