From 9525ea5c3a2aeca0b78851f7e62f32b96823f73a Mon Sep 17 00:00:00 2001 From: "polylane[bot]" <277585245+polylane[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 23:17:15 +0000 Subject: [PATCH] Add structured logging to high-signal failure paths Co-authored-by: polylane[bot] <277585245+polylane[bot]@users.noreply.github.com> --- src/agentic.ts | 12 +++++++++-- src/coding-agent.ts | 48 +++++++++++++++++++++++++++++-------------- src/github-api.ts | 28 ++++++++++++++++++++++--- src/mcp-client.ts | 8 +++++--- src/mcp-shared.ts | 24 ++++++++++++++++++---- src/mcp.ts | 6 +++++- src/skill-registry.ts | 13 ++++++++---- src/user-control.ts | 4 +++- 8 files changed, 110 insertions(+), 33 deletions(-) diff --git a/src/agentic.ts b/src/agentic.ts index ad7a1ea..181b2b2 100644 --- a/src/agentic.ts +++ b/src/agentic.ts @@ -26,6 +26,7 @@ import { createWorkspaceTools, createExecuteTool } from "./think-adapter"; import { createShellTool } from "./tools/shell"; import { sanitizeToolJsonSchema } from "./tool-schema"; import { runTypecheck } from "./typecheck"; +import { log } from "./logger"; import type { AppConfig, Env, TodoStore } from "./types"; /** Options passed through from the coding agent into tool factories. */ @@ -1817,7 +1818,10 @@ function buildOAuthMcpTools( for (const info of oauthTools) { if (!info.name || !info.serverId) { - console.warn("[oauth-mcp] Skipping tool with missing name or serverId:", info); + log("warn", "oauth-mcp skipping tool with missing name or serverId", { + serverId: info.serverId, + name: info.name, + }); continue; } @@ -1827,7 +1831,11 @@ function buildOAuthMcpTools( const prefixedName = fullName.length > 64 ? fullName.slice(0, 64) : fullName; if (existingNames.has(prefixedName) || tools[prefixedName]) { - console.warn("[oauth-mcp] Skipping duplicate tool name:", { prefixedName, serverId: info.serverId, original: info.name }); + log("warn", "oauth-mcp skipping duplicate tool name", { + prefixedName, + serverId: info.serverId, + original: info.name, + }); continue; } diff --git a/src/coding-agent.ts b/src/coding-agent.ts index 47d8bd5..00304dd 100644 --- a/src/coding-agent.ts +++ b/src/coding-agent.ts @@ -2928,7 +2928,10 @@ export class CodingAgent extends Think { } } else if (existing.length > 1) { // Multiple sessions — use most recent, delete extras - console.warn(`CodingAgent: found ${existing.length} Think sessions, expected 1. Using most recent.`); + log("warn", "multiple think sessions found, using most recent", { + sessionId: this.sessionId(), + count: existing.length, + }); const sorted = [...existing].sort((a, b) => b.updated_at.localeCompare(a.updated_at)); // Switch to the most recent this.switchSession(sorted[0].id); @@ -6157,7 +6160,7 @@ export class CodingAgent extends Think { // returns an error. Think's applyChunkToParts silently drops these. // Capture so we can throw after the stream ends. streamError = chunk.errorText ?? chunk.error ?? "Unknown LLM error"; - console.error("[runThinkChat] stream error chunk:", streamError); + log("error", "stream error chunk", { sessionId: this.sessionId(), error: streamError }); } // Token usage is captured via onChatMessage() override — see _lastUsage. } catch { @@ -6178,7 +6181,7 @@ export class CodingAgent extends Think { } }, onError: (error: string) => { - console.error("Think chat error:", error); + log("error", "think chat error", { sessionId: this.sessionId(), error }); streamError = error; }, }; @@ -6305,7 +6308,10 @@ export class CodingAgent extends Think { try { await this.maybeCompactContext(); } catch (err) { - console.warn("[compaction] Post-chat compaction failed:", err instanceof Error ? err.message : err); + log("warn", "post-chat compaction failed", { + sessionId: this.sessionId(), + error: err instanceof Error ? err.message : String(err), + }); } return { assistantMessageId, tokenInput, tokenOutput, text: fullText }; @@ -6654,10 +6660,11 @@ export class CodingAgent extends Think { iterative: !!previousSummary, }); } catch (error) { - console.warn( - "[compaction:ERROR] Failed to generate summary:", - error instanceof Error ? `${error.message}\n${error.stack}` : error, - ); + log("warn", "failed to generate compaction summary", { + sessionId: this.sessionId(), + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }); } } @@ -6963,7 +6970,10 @@ export class CodingAgent extends Think { this._artifactsTokenSecret = tokenSecret; return { repo, remote, tokenSecret }; } catch (err) { - console.warn("[artifacts] failed to get/create repo:", err); + log("warn", "failed to get/create artifacts repo", { + sessionId: this.sessionId(), + error: err instanceof Error ? err.message : String(err), + }); return null; } } @@ -7458,10 +7468,12 @@ export class CodingAgent extends Think { // We also record the failure on `mcpStatus` so the UI can surface it // instead of leaving the user to guess why tools never appeared. const message = error instanceof Error ? error.message : String(error); - console.warn( - `MCP connect failed for "${config.name}" (${config.id}):`, - message, - ); + log("warn", "mcp connect failed", { + sessionId: this.sessionId(), + mcpId: config.id, + mcpName: config.name, + error: message, + }); this.mcpStatus.set(config.id, { name: config.name, url: config.url, @@ -7485,7 +7497,10 @@ export class CodingAgent extends Think { // fingerprint/timestamp in place so the next call retries. this.mcpConnectedAt = 0; this.mcpEnabledConfigsFingerprint = null; - console.warn("connectMcpServers failed:", error instanceof Error ? error.message : error); + log("warn", "connectMcpServers failed", { + sessionId: this.sessionId(), + error: error instanceof Error ? error.message : String(error), + }); } } @@ -7547,7 +7562,10 @@ export class CodingAgent extends Think { }); } catch (error) { // Log but don't throw — sync failure shouldn't break prompt completion - console.error("syncSessionIndex failed:", error instanceof Error ? error.message : error); + log("warn", "syncSessionIndex failed", { + sessionId: this.sessionId(), + error: error instanceof Error ? error.message : String(error), + }); } } diff --git a/src/github-api.ts b/src/github-api.ts index 9b15bd9..d336873 100644 --- a/src/github-api.ts +++ b/src/github-api.ts @@ -493,7 +493,20 @@ export async function triggerVerifyWorkflow(input: { if (!triggerRes.ok) { const errorText = await triggerRes.text(); - console.warn(`[verify-gate] workflow_dispatch failed ${triggerRes.status}:`, errorText.slice(0, 500)); + let message = ""; + try { + const parsed = JSON.parse(errorText) as { message?: unknown }; + if (typeof parsed.message === "string") message = parsed.message; + } catch { + // Non-JSON error body — omit rather than forward raw text to the log sink. + } + log("warn", "verify-gate workflow_dispatch failed", { + runId: run.id, + repo: run.repoUrl, + branch: run.branch, + status: triggerRes.status, + error: message, + }); return null; } @@ -520,7 +533,11 @@ export async function triggerVerifyWorkflow(input: { } } - console.warn("[verify-gate] workflow dispatched but run id not found within 5s"); + log("warn", "verify-gate workflow dispatched but run id not found", { + runId: run.id, + repo: run.repoUrl, + branch: run.branch, + }); return null; } @@ -564,7 +581,12 @@ export async function pollVerifyWorkflow(input: { const url = `https://api.github.com/repos/${parsed.owner}/${parsed.repo}/actions/runs/${encodeURIComponent(run.verifyWorkflowRunId)}`; const res = await fetch(url, { headers: ghHeaders(token) }); if (!res.ok) { - console.warn(`[verify-gate] poll failed ${res.status}`); + log("warn", "verify-gate poll failed", { + runId: run.id, + workflowRunId: run.verifyWorkflowRunId, + repo: run.repoUrl, + status: res.status, + }); return null; } diff --git a/src/mcp-client.ts b/src/mcp-client.ts index 9a34592..0eb804b 100644 --- a/src/mcp-client.ts +++ b/src/mcp-client.ts @@ -9,6 +9,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/ // outputSchema, which is why their MCP server's tools never appeared // despite a working connect/initialize.) import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/cfworker-provider.js"; +import { log } from "./logger"; // ─── Auth header normalisation ─── @@ -140,9 +141,10 @@ export class HttpMcpClient implements McpClient { const norm = normaliseAuthHeaders(headers); headers = norm.headers; if (norm.normalised) { - console.info( - `[mcp] prepended "Bearer " to Authorization header for "${this.config.name}" (${this.config.id}) — stored value was missing an auth scheme`, - ); + log("info", "prepended Bearer auth scheme to MCP Authorization header", { + mcpId: this.config.id, + mcpName: this.config.name, + }); } // Propagate MCP recursion depth to outbound MCP servers if (this.mcpDepth > 0) { diff --git a/src/mcp-shared.ts b/src/mcp-shared.ts index 6c3ac6c..b8fa439 100644 --- a/src/mcp-shared.ts +++ b/src/mcp-shared.ts @@ -3,6 +3,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { canonicalizeEmail, resolveAdminEmail } from "./auth"; import { chatMonitorIdName, sendChatReaction, sendChatReply } from "./chat-monitor-agent"; +import { log } from "./logger"; import type { Env } from "./types"; /** @@ -20,7 +21,7 @@ export function mcpUserEmail(env: Env, userEmail?: string, label = "mcp"): strin if (canonical) return canonical; const email = resolveAdminEmail(env); if (!email) throw new Error("ADMIN_EMAIL must be configured for MCP access. Set it as a secret or in wrangler.jsonc vars."); - console.warn(`[${label}] Operation attributed to admin via service-mode fallback (no userEmail threaded).`); + log("warn", "mcp operation attributed to admin via service-mode fallback", { label }); return email; } @@ -89,7 +90,12 @@ export function registerChatReplyTool(server: McpServer, env: Env, userEmail: st emoji: reactionEmoji, action: "remove", }).catch((err) => { - console.warn("[chat_reply] failed to remove loading reaction (non-fatal):", err instanceof Error ? err.message : String(err)); + log("warn", "chat_reply failed to remove loading reaction", { + sessionId, + spaceId: flag.spaceId, + messageName, + error: err instanceof Error ? err.message : String(err), + }); }); // Add the done reaction only when we actually posted a reply. @@ -99,7 +105,12 @@ export function registerChatReplyTool(server: McpServer, env: Env, userEmail: st emoji: doneEmoji, action: "add", }).catch((err) => { - console.warn("[chat_reply] failed to add done reaction (non-fatal):", err instanceof Error ? err.message : String(err)); + log("warn", "chat_reply failed to add done reaction", { + sessionId, + spaceId: flag.spaceId, + messageName, + error: err instanceof Error ? err.message : String(err), + }); }); } @@ -113,7 +124,12 @@ export function registerChatReplyTool(server: McpServer, env: Env, userEmail: st body: JSON.stringify({ messageName }), }); } catch (err) { - console.warn("[chat_reply] failed to notify monitor of reaction clear (non-fatal):", err instanceof Error ? err.message : String(err)); + log("warn", "chat_reply failed to notify monitor of reaction clear", { + sessionId, + spaceId: flag.spaceId, + messageName, + error: err instanceof Error ? err.message : String(err), + }); } } diff --git a/src/mcp.ts b/src/mcp.ts index 6ec7b0c..2e1d6aa 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -763,7 +763,11 @@ export function createDodoMcpServer(env: Env, userEmail: string, depth = 0): Mcp await sourceCtx.repo.fork(`dodo-${newId}`, { defaultBranchOnly: false }); } } catch (err) { - console.warn("[fork_session] Artifacts fork failed (files still copied via snapshot):", err); + log("warn", "fork_session artifacts fork failed", { + sessionId, + newId, + error: err instanceof Error ? err.message : String(err), + }); } return textResult({ sessionId: newId, sourceSessionId: sessionId, forkedAt: new Date().toISOString() }); }); diff --git a/src/skill-registry.ts b/src/skill-registry.ts index a7088e6..a3cbc52 100644 --- a/src/skill-registry.ts +++ b/src/skill-registry.ts @@ -27,6 +27,7 @@ */ import type { Workspace } from "@cloudflare/shell"; +import { log } from "./logger"; // ─── Types ─── @@ -221,9 +222,10 @@ export function normalizeFrontmatter( if (options.dirName && options.dirName !== name) { // Loose match — log a warning but don't throw. OpenCode is strict here; // we're more forgiving so imported third-party skills don't fail. - console.warn( - `SKILL.md name "${name}" doesn't match directory "${options.dirName}" — proceeding anyway`, - ); + log("warn", "skill name mismatch with directory", { + name, + dirName: options.dirName, + }); } const descRaw = frontmatter.description; @@ -434,7 +436,10 @@ async function loadWorkspaceSkill( rawFrontmatter: input.rawFrontmatter, }; } catch (error) { - console.warn(`skill-load-failed dir=${dir}:`, error instanceof Error ? error.message : error); + log("warn", "skill-load-failed", { + dir, + error: error instanceof Error ? error.message : String(error), + }); return null; } } diff --git a/src/user-control.ts b/src/user-control.ts index 6c79fd7..189adb3 100644 --- a/src/user-control.ts +++ b/src/user-control.ts @@ -2999,7 +2999,9 @@ export class UserControl extends DurableObject { const sharedIndex = this.env.SHARED_INDEX.get(this.env.SHARED_INDEX.idFromName("global")); await sharedIndex.fetch(`https://shared-index/mcp-token-index/${encodeURIComponent(token)}`, { method: "DELETE" }); } catch (err) { - console.warn("[user-control] Failed to delete token from SharedIndex:", err); + log("warn", "failed to delete token from SharedIndex", { + error: err instanceof Error ? err.message : String(err), + }); } return true; }