diff --git a/apps/code/src/renderer/features/sessions/components/buildConversationItems.test.ts b/apps/code/src/renderer/features/sessions/components/buildConversationItems.test.ts index 2e115eb63..ff0061f4c 100644 --- a/apps/code/src/renderer/features/sessions/components/buildConversationItems.test.ts +++ b/apps/code/src/renderer/features/sessions/components/buildConversationItems.test.ts @@ -1,7 +1,66 @@ import type { AcpMessage } from "@shared/types/session-events"; import { makeAttachmentUri } from "@utils/promptContent"; import { describe, expect, it } from "vitest"; -import { buildConversationItems } from "./buildConversationItems"; +import { + buildConversationItems, + type ConversationItem, +} from "./buildConversationItems"; + +function consoleMsg(ts: number, message: string, level = "info"): AcpMessage { + return { + type: "acp_message", + ts, + message: { + jsonrpc: "2.0", + method: "_posthog/console", + params: { level, message }, + }, + }; +} + +function progressMsg( + ts: number, + step: string, + status: string, + label: string, + detail?: string, + group = "setup", +): AcpMessage { + return { + type: "acp_message", + ts, + message: { + jsonrpc: "2.0", + method: "_posthog/progress", + params: { step, status, label, detail, group }, + }, + }; +} + +function userPromptMsg(ts: number, id: number, text: string): AcpMessage { + return { + type: "acp_message", + ts, + message: { + jsonrpc: "2.0", + id, + method: "session/prompt", + params: { prompt: [{ type: "text", text }] }, + }, + }; +} + +function promptResponseMsg(ts: number, id: number): AcpMessage { + return { + type: "acp_message", + ts, + message: { + jsonrpc: "2.0", + id, + result: { stopReason: "end_turn" }, + }, + }; +} describe("buildConversationItems", () => { it("extracts cloud prompt attachments into user messages", () => { @@ -137,4 +196,228 @@ describe("buildConversationItems", () => { }, ]); }); + + describe("progress notifications", () => { + it("aggregates progress events arriving before the first prompt into one progress_group item in arrival order", () => { + const events: AcpMessage[] = [ + progressMsg(1, "sandbox", "in_progress", "Setting up sandbox"), + progressMsg(2, "sandbox", "completed", "Set up sandbox"), + progressMsg(3, "clone", "in_progress", "Cloning repository"), + progressMsg(4, "clone", "completed", "Cloned repository"), + progressMsg(5, "checkout", "in_progress", "Checking out branch main"), + ]; + + const result = buildConversationItems(events, null); + + const groups = findProgressGroups(result.items); + expect(groups).toHaveLength(1); + const update = groups[0]; + expect(update.steps.map((s) => [s.key, s.status, s.label])).toEqual([ + ["sandbox", "completed", "Set up sandbox"], + ["clone", "completed", "Cloned repository"], + ["checkout", "in_progress", "Checking out branch main"], + ]); + expect(update.isActive).toBe(true); + }); + + it("marks the progress group inactive once no step is in_progress", () => { + const events: AcpMessage[] = [ + progressMsg(1, "sandbox", "completed", "Set up sandbox"), + progressMsg(2, "clone", "completed", "Cloned repository"), + progressMsg(3, "agent", "completed", "Started agent"), + ]; + + const result = buildConversationItems(events, null); + const [group] = findProgressGroups(result.items); + expect(group.isActive).toBe(false); + }); + + it("opens a separate progress_group per group id — distinct groups coexist inline", () => { + const events: AcpMessage[] = [ + // Pre-prompt setup group. + progressMsg( + 1, + "sandbox", + "in_progress", + "Setting up sandbox", + undefined, + "setup", + ), + progressMsg( + 2, + "sandbox", + "completed", + "Set up sandbox", + undefined, + "setup", + ), + // First user prompt + response. + userPromptMsg(10, 1, "hi"), + promptResponseMsg(20, 1), + // A distinct group id — must open its own card, not join "setup". + progressMsg( + 30, + "push", + "in_progress", + "Creating pull request", + undefined, + "pr_create", + ), + progressMsg( + 40, + "push", + "completed", + "Created pull request", + undefined, + "pr_create", + ), + ]; + + const result = buildConversationItems(events, null); + const groups = findProgressGroups(result.items); + expect(groups).toHaveLength(2); + + expect(groups[0].steps.map((s) => s.key)).toEqual(["sandbox"]); + expect(groups[0].isActive).toBe(false); + + expect(groups[1].steps.map((s) => [s.key, s.status, s.label])).toEqual([ + ["push", "completed", "Created pull request"], + ]); + expect(groups[1].isActive).toBe(false); + }); + + it("late completion events update the original group regardless of turn boundaries", () => { + const events: AcpMessage[] = [ + // `sandbox` starts in the pre-prompt implicit turn. + progressMsg( + 1, + "sandbox", + "in_progress", + "Setting up sandbox", + undefined, + "setup", + ), + // User prompt + response come in before the completion lands. + userPromptMsg(10, 1, "hi"), + promptResponseMsg(20, 1), + // The completion arrives late, after the turn boundary — it should + // still update the existing "setup" card, not open a new one. + progressMsg( + 30, + "sandbox", + "completed", + "Set up sandbox", + undefined, + "setup", + ), + ]; + + const result = buildConversationItems(events, null); + const groups = findProgressGroups(result.items); + expect(groups).toHaveLength(1); + expect(groups[0].steps).toEqual([ + { + key: "sandbox", + status: "completed", + label: "Set up sandbox", + detail: undefined, + }, + ]); + expect(groups[0].isActive).toBe(false); + }); + + it("drops progress events missing a group id", () => { + const events: AcpMessage[] = [ + { + type: "acp_message", + ts: 1, + message: { + jsonrpc: "2.0", + method: "_posthog/progress", + params: { + step: "sandbox", + status: "in_progress", + label: "Setting up sandbox", + }, + }, + }, + ]; + + const result = buildConversationItems(events, null); + expect(findProgressGroups(result.items)).toHaveLength(0); + }); + + it("replaces the step entry when a later event revisits the same key with a new label/status", () => { + const events: AcpMessage[] = [ + progressMsg(1, "sandbox", "in_progress", "Setting up sandbox"), + progressMsg(2, "sandbox", "failed", "Set up failed", "timeout"), + ]; + + const result = buildConversationItems(events, null); + const [group] = findProgressGroups(result.items); + expect(group.steps).toHaveLength(1); + expect(group.steps[0]).toEqual({ + key: "sandbox", + status: "failed", + label: "Set up failed", + detail: "timeout", + }); + }); + + it("hides debug-level console logs by default and renders them inline when showDebugLogs is true", () => { + const events: AcpMessage[] = [ + progressMsg(1, "sandbox", "in_progress", "Setting up sandbox"), + consoleMsg(2, "sandbox provisioned", "debug"), + ]; + + const hidden = buildConversationItems(events, null); + expect( + hidden.items.some( + (i) => + i.type === "session_update" && i.update.sessionUpdate === "console", + ), + ).toBe(false); + + const shown = buildConversationItems(events, null, { + showDebugLogs: true, + }); + expect( + shown.items.some( + (i) => + i.type === "session_update" && i.update.sessionUpdate === "console", + ), + ).toBe(true); + }); + + it("emits no progress group for a conversation without progress notifications", () => { + const events: AcpMessage[] = [userPromptMsg(1, 1, "hi")]; + + const result = buildConversationItems(events, null); + expect(findProgressGroups(result.items)).toHaveLength(0); + }); + }); }); + +// Local alias kept intentionally narrow to the shape we care about in tests. +type RenderItemUnion = Extract< + ConversationItem, + { type: "session_update" } +>["update"]; + +type ProgressGroupUpdate = Extract< + RenderItemUnion, + { sessionUpdate: "progress_group" } +>; + +function findProgressGroups(items: ConversationItem[]): ProgressGroupUpdate[] { + const groups: ProgressGroupUpdate[] = []; + for (const item of items) { + if ( + item.type === "session_update" && + item.update.sessionUpdate === "progress_group" + ) { + groups.push(item.update); + } + } + return groups; +} diff --git a/apps/code/src/renderer/features/sessions/components/buildConversationItems.ts b/apps/code/src/renderer/features/sessions/components/buildConversationItems.ts index 7e5d972e5..c94d58464 100644 --- a/apps/code/src/renderer/features/sessions/components/buildConversationItems.ts +++ b/apps/code/src/renderer/features/sessions/components/buildConversationItems.ts @@ -51,6 +51,15 @@ export type ConversationItem = | UserShellExecute | { type: "queued"; id: string; message: QueuedMessage }; +export type ProgressStatus = "in_progress" | "completed" | "failed"; + +export interface ProgressStep { + key: string; + status: ProgressStatus; + label: string; + detail?: string; +} + export interface LastTurnInfo { isComplete: boolean; durationMs: number; @@ -63,6 +72,17 @@ export interface BuildResult { isCompacting: boolean; } +interface ProgressCardState { + /** Step key → full step entry. Key order reflects arrival order. */ + steps: Map; + /** Reference to the pushed render item; mutated in place as events arrive. */ + renderItem: { + sessionUpdate: "progress_group"; + steps: ProgressStep[]; + isActive: boolean; + }; +} + interface TurnState { id: string; promptId: number; @@ -83,6 +103,11 @@ interface ItemBuilder { shellExecutes: Map; isCompacting: boolean; nextId: () => number; + /** Progress cards keyed by the backend-supplied `group` id. The first event + * for a group opens the card inline where it arrived; every subsequent + * event for the same id mutates the same card, regardless of which turn is + * currently active. */ + progressCards: Map; } function createItemBuilder(): ItemBuilder { @@ -94,6 +119,7 @@ function createItemBuilder(): ItemBuilder { shellExecutes: new Map(), isCompacting: false, nextId: () => idCounter++, + progressCards: new Map(), }; } @@ -136,6 +162,7 @@ function pushItem(b: ItemBuilder, update: RenderItem) { } export interface BuildConversationOptions { + /** Render `debug`-level console logs inline; without this only info/warn/error show up. */ showDebugLogs?: boolean; } @@ -331,21 +358,27 @@ function handleNotification( } if (isNotification(msg.method, POSTHOG_NOTIFICATIONS.CONSOLE)) { - if (!b.currentTurn) { - ensureImplicitTurn(b, ts); - } const params = msg.params as { level?: string; message?: string }; if (!params?.message) return; - if (params.level === "debug" && !options?.showDebugLogs) return; + const level = params.level ?? "info"; + // Cloud runs downgrade every console log to debug at the source, so this + // gate hides the entire stream unless the user flips the debug toggle. + if (level === "debug" && !options?.showDebugLogs) return; + if (!b.currentTurn) ensureImplicitTurn(b, ts); pushItem(b, { sessionUpdate: "console", - level: params.level ?? "info", + level, message: params.message, timestamp: new Date(ts).toISOString(), }); return; } + if (isNotification(msg.method, POSTHOG_NOTIFICATIONS.PROGRESS)) { + handleProgress(b, msg.params, ts); + return; + } + if (isNotification(msg.method, POSTHOG_NOTIFICATIONS.COMPACT_BOUNDARY)) { if (!b.currentTurn) ensureImplicitTurn(b, ts); const params = msg.params as { @@ -378,6 +411,72 @@ function handleNotification( } } +function ensureProgressCardForGroup( + b: ItemBuilder, + group: string, + ts: number, +): ProgressCardState | null { + const existing = b.progressCards.get(group); + if (existing) return existing; + + if (!b.currentTurn) ensureImplicitTurn(b, ts); + if (!b.currentTurn) return null; + + const renderItem = { + sessionUpdate: "progress_group" as const, + steps: [] as ProgressStep[], + isActive: true, + }; + const card: ProgressCardState = { + steps: new Map(), + renderItem, + }; + b.progressCards.set(group, card); + pushItem(b, renderItem); + return card; +} + +function syncProgressCard(card: ProgressCardState) { + const ordered: ProgressStep[] = Array.from(card.steps.values()); + card.renderItem.steps = ordered; + card.renderItem.isActive = ordered.some((s) => s.status === "in_progress"); +} + +function handleProgress(b: ItemBuilder, rawParams: unknown, ts: number) { + const params = rawParams as + | { + step?: string; + status?: string; + label?: string; + detail?: string; + group?: string; + } + | undefined; + if (!params?.step || !params.label || !params.group) return; + + const status = normalizeProgressStatus(params.status); + const card = ensureProgressCardForGroup(b, params.group, ts); + if (!card) return; + card.steps.set(params.step, { + key: params.step, + status, + label: params.label, + detail: params.detail, + }); + syncProgressCard(card); +} + +function normalizeProgressStatus(raw: string | undefined): ProgressStatus { + switch (raw) { + case "in_progress": + case "completed": + case "failed": + return raw; + default: + return "in_progress"; + } +} + function markCompactingStatusComplete(b: ItemBuilder) { b.isCompacting = false; for (let i = b.items.length - 1; i >= 0; i--) { diff --git a/apps/code/src/renderer/features/sessions/components/session-update/ProgressGroupView.tsx b/apps/code/src/renderer/features/sessions/components/session-update/ProgressGroupView.tsx new file mode 100644 index 000000000..cb1d2b8b2 --- /dev/null +++ b/apps/code/src/renderer/features/sessions/components/session-update/ProgressGroupView.tsx @@ -0,0 +1,131 @@ +import type { ProgressStep } from "@features/sessions/components/buildConversationItems"; +import { + CaretDownIcon, + CaretRightIcon, + CheckCircleIcon, + CircleIcon, + CircleNotchIcon, + XCircleIcon, +} from "@phosphor-icons/react"; +import * as Collapsible from "@radix-ui/react-collapsible"; +import { Box, Flex, Text } from "@radix-ui/themes"; +import { useEffect, useState } from "react"; + +interface ProgressGroupViewProps { + steps: ProgressStep[]; + /** True while at least one step in this group is `in_progress`. */ + isActive: boolean; + /** True once the enclosing turn has finished. Drives the auto-collapse. */ + turnComplete?: boolean; +} + +type ProgressStatus = ProgressStep["status"]; + +function StepIcon({ status }: { status: ProgressStatus }) { + switch (status) { + case "in_progress": + return ; + case "completed": + return ( + + ); + case "failed": + return ; + default: + return ; + } +} + +// Header label follows the stream: the currently in-flight step's label if +// any, otherwise the last step seen. No hardcoded fallbacks — the backend +// controls all wording, including present-tense during `in_progress`. +function resolveHeaderLabel(steps: ProgressStep[]): string | null { + if (steps.length === 0) return null; + const active = steps.find((s) => s.status === "in_progress"); + if (active) return active.label; + return steps[steps.length - 1].label; +} + +export function ProgressGroupView({ + steps, + isActive, + turnComplete, +}: ProgressGroupViewProps) { + // Multi-step groups always render a collapsible header (caret + summary). + // While the turn is still running the trigger is disabled and forced open, + // so the user sees progress stream in without a flicker between consecutive + // step transitions. Once the turn completes, the header auto-collapses and + // becomes interactive. Single-step groups have no header at all — the one + // step row IS the whole view. + const [userToggledOpen, setUserToggledOpen] = useState(null); + + useEffect(() => { + // Any reactivation clears the sticky user choice so a new round of work + // starts expanded again. + if (isActive) setUserToggledOpen(null); + }, [isActive]); + + if (steps.length === 0) return null; + + const hasHeader = steps.length > 1; + // Single-step groups have no header, so their body must stay expanded — + // collapsing with no header would leave nothing on screen. Multi-step groups + // stay open while the turn is running, then honour the user toggle once the + // turn completes (default: collapsed). + const isOpen = !hasHeader + ? true + : !turnComplete + ? true + : (userToggledOpen ?? true); + const summaryLabel = resolveHeaderLabel(steps) ?? ""; + + return ( + + { + if (hasHeader && turnComplete) setUserToggledOpen(next); + }} + > + {hasHeader && ( + + + + )} + + + {steps.map((step) => ( + + + + + {step.label} + + + {step.detail && ( + + + {step.detail} + + + )} + + ))} + + + + + ); +} diff --git a/apps/code/src/renderer/features/sessions/components/session-update/SessionUpdateView.tsx b/apps/code/src/renderer/features/sessions/components/session-update/SessionUpdateView.tsx index 51fa6112e..6d2f99043 100644 --- a/apps/code/src/renderer/features/sessions/components/session-update/SessionUpdateView.tsx +++ b/apps/code/src/renderer/features/sessions/components/session-update/SessionUpdateView.tsx @@ -1,4 +1,7 @@ -import type { ConversationItem } from "@features/sessions/components/buildConversationItems"; +import type { + ConversationItem, + ProgressStep, +} from "@features/sessions/components/buildConversationItems"; import type { SessionUpdate, ToolCall } from "@features/sessions/types"; import { memo } from "react"; @@ -6,6 +9,7 @@ import { AgentMessage } from "./AgentMessage"; import { CompactBoundaryView } from "./CompactBoundaryView"; import { ConsoleMessage } from "./ConsoleMessage"; import { ErrorNotificationView } from "./ErrorNotificationView"; +import { ProgressGroupView } from "./ProgressGroupView"; import { StatusNotificationView } from "./StatusNotificationView"; import { TaskNotificationView } from "./TaskNotificationView"; import { ThoughtView } from "./ThoughtView"; @@ -41,6 +45,11 @@ export type RenderItem = status: "completed" | "failed" | "stopped"; summary: string; outputFile: string; + } + | { + sessionUpdate: "progress_group"; + steps: ProgressStep[]; + isActive: boolean; }; interface SessionUpdateViewProps { @@ -123,6 +132,14 @@ export const SessionUpdateView = memo(function SessionUpdateView({ return ( ); + case "progress_group": + return ( + + ); default: return null; } diff --git a/packages/agent/src/acp-extensions.ts b/packages/agent/src/acp-extensions.ts index cd1f1cc7e..13e0ce87d 100644 --- a/packages/agent/src/acp-extensions.ts +++ b/packages/agent/src/acp-extensions.ts @@ -55,6 +55,9 @@ export const POSTHOG_NOTIFICATIONS = { /** Agent status update (thinking, working, etc.) */ STATUS: "_posthog/status", + /** Structured backend progress notification; events in the same turn group into one card on the client */ + PROGRESS: "_posthog/progress", + /** Task-level notification (progress, milestones) */ TASK_NOTIFICATION: "_posthog/task_notification", diff --git a/packages/agent/src/server/agent-server.ts b/packages/agent/src/server/agent-server.ts index 5bf8ea373..17c19a974 100644 --- a/packages/agent/src/server/agent-server.ts +++ b/packages/agent/src/server/agent-server.ts @@ -460,7 +460,9 @@ export class AgentServer { port: this.config.port, }, () => { - this.logger.info(`HTTP server listening on port ${this.config.port}`); + this.logger.debug( + `HTTP server listening on port ${this.config.port}`, + ); resolve(); }, ); @@ -472,12 +474,12 @@ export class AgentServer { private async autoInitializeSession(): Promise { const { taskId, runId, mode, projectId } = this.config; - this.logger.info("Auto-initializing session", { taskId, runId, mode }); + this.logger.debug("Auto-initializing session", { taskId, runId, mode }); // Check if this is a resume from a previous run const resumeRunId = process.env.POSTHOG_RESUME_RUN_ID; if (resumeRunId) { - this.logger.info("Resuming from previous run", { + this.logger.debug("Resuming from previous run", { resumeRunId, currentRunId: runId, }); @@ -489,13 +491,13 @@ export class AgentServer { apiClient: this.posthogAPI, logger: new Logger({ debug: true, prefix: "[Resume]" }), }); - this.logger.info("Resume state loaded", { + this.logger.debug("Resume state loaded", { conversationTurns: this.resumeState.conversation.length, snapshotApplied: this.resumeState.snapshotApplied, logEntries: this.resumeState.logEntryCount, }); } catch (error) { - this.logger.warn("Failed to load resume state, starting fresh", { + this.logger.debug("Failed to load resume state, starting fresh", { error, }); this.resumeState = null; @@ -516,7 +518,7 @@ export class AgentServer { } async stop(): Promise { - this.logger.info("Stopping agent server..."); + this.logger.debug("Stopping agent server..."); if (this.session) { await this.cleanupSession(); @@ -527,7 +529,7 @@ export class AgentServer { this.server = null; } - this.logger.info("Agent server stopped"); + this.logger.debug("Agent server stopped"); } private authenticateRequest( @@ -589,7 +591,7 @@ export class AgentServer { }); const promptPreview = promptBlocksToText(prompt); - this.logger.info( + this.logger.debug( `Processing user message (detectedPrUrl=${this.detectedPrUrl ?? "none"}): ${promptPreview.substring(0, 100)}...`, ); @@ -607,7 +609,7 @@ export class AgentServer { }), }); - this.logger.info("User message completed", { + this.logger.debug("User message completed", { stopReason: result.stopReason, }); @@ -621,7 +623,7 @@ export class AgentServer { // Relay the response to Slack. For follow-ups this is the primary // delivery path — the HTTP caller only handles reactions. this.relayAgentResponse(this.session.payload).catch((err) => - this.logger.warn("Failed to relay follow-up response", err), + this.logger.debug("Failed to relay follow-up response", err), ); } @@ -637,7 +639,7 @@ export class AgentServer { this.session.payload.run_id, ); } catch { - this.logger.warn("Failed to extract assistant message from logs"); + this.logger.debug("Failed to extract assistant message from logs"); } return { @@ -648,7 +650,7 @@ export class AgentServer { case POSTHOG_NOTIFICATIONS.CANCEL: case "cancel": { - this.logger.info("Cancel requested", { + this.logger.debug("Cancel requested", { acpSessionId: this.session.acpSessionId, }); await this.session.clientConnection.cancel({ @@ -659,7 +661,7 @@ export class AgentServer { case POSTHOG_NOTIFICATIONS.CLOSE: case "close": { - this.logger.info("Close requested"); + this.logger.debug("Close requested"); await this.cleanupSession(); return { closed: true }; } @@ -669,7 +671,7 @@ export class AgentServer { const configId = params.configId as string; const value = params.value as string; - this.logger.info("Set config option requested", { configId, value }); + this.logger.debug("Set config option requested", { configId, value }); const result = await this.session.clientConnection.setSessionConfigOption({ @@ -707,7 +709,7 @@ export class AgentServer { const customInput = params.customInput as string | undefined; const answers = params.answers as Record | undefined; - this.logger.info("Permission response received", { + this.logger.debug("Permission response received", { requestId, optionId, }); @@ -742,7 +744,7 @@ export class AgentServer { // duplicate Slack messages. This lock ensures the second caller waits for the first // initialization to finish and reuses the session. if (this.initializationPromise) { - this.logger.info("Waiting for in-progress initialization", { + this.logger.debug("Waiting for in-progress initialization", { runId: payload.run_id, }); await this.initializationPromise; @@ -774,7 +776,7 @@ export class AgentServer { await this.cleanupSession(); } - this.logger.info("Initializing session", { + this.logger.debug("Initializing session", { runId: payload.run_id, taskId: payload.task_id, }); @@ -790,7 +792,7 @@ export class AgentServer { this.posthogAPI .getTaskRun(payload.task_id, payload.run_id) .catch((err) => { - this.logger.warn("Failed to fetch task run for session context", { + this.logger.debug("Failed to fetch task run for session context", { taskId: payload.task_id, runId: payload.run_id, error: err, @@ -798,7 +800,7 @@ export class AgentServer { return null; }), this.posthogAPI.getTask(payload.task_id).catch((err) => { - this.logger.warn("Failed to fetch task for session context", { + this.logger.debug("Failed to fetch task for session context", { taskId: payload.task_id, error: err, }); @@ -941,7 +943,7 @@ export class AgentServer { }); const acpSessionId = sessionResponse.sessionId; - this.logger.info("ACP session created", { + this.logger.debug("ACP session created", { acpSessionId, runId: payload.run_id, }); @@ -963,18 +965,15 @@ export class AgentServer { debug: true, prefix: "[AgentServer]", onLog: (level, scope, message, data) => { - // Preserve console output (onLog suppresses default console.*) - const _formatted = - data !== undefined ? `${message} ${JSON.stringify(data)}` : message; this.emitConsoleLog(level, scope, message, data); }, }); - this.logger.info("Session initialized successfully"); - this.logger.info( + this.logger.debug("Session initialized successfully"); + this.logger.debug( `Agent version: ${this.config.version ?? packageJson.version}`, ); - this.logger.info(`Initial permission mode: ${initialPermissionMode}`); + this.logger.debug(`Initial permission mode: ${initialPermissionMode}`); // Signal in_progress so the UI can start polling for updates this.posthogAPI @@ -982,7 +981,7 @@ export class AgentServer { status: "in_progress", }) .catch((err) => - this.logger.warn("Failed to set task run to in_progress", err), + this.logger.debug("Failed to set task run to in_progress", err), ); await this.sendInitialTaskMessage(payload, preTaskRun); @@ -1038,7 +1037,7 @@ export class AgentServer { payload.run_id, ); } catch (error) { - this.logger.warn("Failed to fetch task run", { + this.logger.debug("Failed to fetch task run", { taskId: payload.task_id, runId: payload.run_id, error, @@ -1050,7 +1049,7 @@ export class AgentServer { if (!this.resumeState) { const resumeRunId = this.getResumeRunId(taskRun); if (resumeRunId) { - this.logger.info("Resuming from previous run (via TaskRun state)", { + this.logger.debug("Resuming from previous run (via TaskRun state)", { resumeRunId, currentRunId: payload.run_id, }); @@ -1062,13 +1061,13 @@ export class AgentServer { apiClient: this.posthogAPI, logger: new Logger({ debug: true, prefix: "[Resume]" }), }); - this.logger.info("Resume state loaded (via TaskRun state)", { + this.logger.debug("Resume state loaded (via TaskRun state)", { conversationTurns: this.resumeState.conversation.length, snapshotApplied: this.resumeState.snapshotApplied, logEntries: this.resumeState.logEntryCount, }); } catch (error) { - this.logger.warn("Failed to load resume state, starting fresh", { + this.logger.debug("Failed to load resume state, starting fresh", { error, }); this.resumeState = null; @@ -1099,11 +1098,11 @@ export class AgentServer { } if (initialPrompt.length === 0) { - this.logger.warn("Task has no description, skipping initial message"); + this.logger.debug("Task has no description, skipping initial message"); return; } - this.logger.info("Sending initial task message", { + this.logger.debug("Sending initial task message", { taskId: payload.task_id, descriptionLength: promptBlocksToText(initialPrompt).length, usedInitialPromptOverride: !!initialPromptOverride, @@ -1117,7 +1116,7 @@ export class AgentServer { prompt: initialPrompt, }); - this.logger.info("Initial task message completed", { + this.logger.debug("Initial task message completed", { stopReason: result.stopReason, }); @@ -1190,7 +1189,7 @@ export class AgentServer { ]; } - this.logger.info("Sending resume message", { + this.logger.debug("Sending resume message", { taskId: payload.task_id, conversationTurns: this.resumeState.conversation.length, promptLength: promptBlocksToText(resumePromptBlocks).length, @@ -1208,7 +1207,7 @@ export class AgentServer { prompt: resumePromptBlocks, }); - this.logger.info("Resume message completed", { + this.logger.debug("Resume message completed", { stopReason: result.stopReason, }); @@ -1674,7 +1673,7 @@ ${attributionInstructions} try { return await getCurrentBranch(this.config.repositoryPath); } catch (error) { - this.logger.warn("Failed to determine current git branch", { + this.logger.debug("Failed to determine current git branch", { repositoryPath: this.config.repositoryPath, error, }); @@ -1695,7 +1694,7 @@ ${attributionInstructions} }); this.lastReportedBranch = branchName; } catch (error) { - this.logger.warn("Failed to attach current branch to task run", { + this.logger.debug("Failed to attach current branch to task run", { taskId: payload.task_id, runId: payload.run_id, branchName, @@ -1715,7 +1714,7 @@ ${attributionInstructions} coalesce: true, }); } catch (error) { - this.logger.warn("Failed to flush session logs before completion", { + this.logger.debug("Failed to flush session logs before completion", { taskId: payload.task_id, runId: payload.run_id, error, @@ -1724,7 +1723,7 @@ ${attributionInstructions} } if (stopReason !== "error") { - this.logger.info("Skipping status update for non-error stop reason", { + this.logger.debug("Skipping status update for non-error stop reason", { stopReason, }); return; @@ -1737,7 +1736,7 @@ ${attributionInstructions} status, error_message: errorMessage ?? "Agent error", }); - this.logger.info("Task completion signaled", { status, stopReason }); + this.logger.debug("Task completion signaled", { status, stopReason }); } catch (error) { this.logger.error("Failed to signal task completion", error); } @@ -1874,7 +1873,7 @@ ${attributionInstructions} isPlanApproval || (needsDesktopApproval && this.session?.hasDesktopConnected) ) { - this.logger.info("Relaying permission request", { + this.logger.debug("Relaying permission request", { kind: params.toolCall?.kind, isQuestion, hasDesktopConnected: this.session?.hasDesktopConnected ?? false, @@ -1919,7 +1918,7 @@ ${attributionInstructions} ) { this.session.permissionMode = params.update .currentModeId as PermissionMode; - this.logger.info("Permission mode updated", { + this.logger.debug("Permission mode updated", { mode: params.update.currentModeId, }); } @@ -1965,7 +1964,7 @@ ${attributionInstructions} try { await this.session.logWriter.flush(payload.run_id, { coalesce: true }); } catch (error) { - this.logger.warn("Failed to flush logs before Slack relay", { + this.logger.debug("Failed to flush logs before Slack relay", { taskId: payload.task_id, runId: payload.run_id, error, @@ -1974,7 +1973,7 @@ ${attributionInstructions} const message = this.session.logWriter.getFullAgentResponse(payload.run_id); if (!message) { - this.logger.warn("No agent message found for Slack relay", { + this.logger.debug("No agent message found for Slack relay", { taskId: payload.task_id, runId: payload.run_id, sessionRegistered: this.session.logWriter.isRegistered(payload.run_id), @@ -1989,7 +1988,7 @@ ${attributionInstructions} message, ); } catch (error) { - this.logger.warn("Failed to relay initial agent response to Slack", { + this.logger.debug("Failed to relay initial agent response to Slack", { taskId: payload.task_id, runId: payload.run_id, error, @@ -2022,7 +2021,7 @@ ${attributionInstructions} this.posthogAPI .relayMessage(payload.task_id, payload.run_id, message) .catch((err) => - this.logger.warn("Failed to relay question to Slack", { err }), + this.logger.debug("Failed to relay question to Slack", { err }), ); } @@ -2120,7 +2119,7 @@ ${attributionInstructions} const prUrl = prUrlMatch[0]; this.detectedPrUrl = prUrl; - this.logger.info("Detected PR URL in bash output", { + this.logger.debug("Detected PR URL in bash output", { runId: payload.run_id, prUrl, }); @@ -2131,7 +2130,7 @@ ${attributionInstructions} output: { pr_url: prUrl }, }) .then(() => { - this.logger.info("PR URL attached to task run", { + this.logger.debug("PR URL attached to task run", { taskId: payload.task_id, runId: payload.run_id, prUrl, @@ -2157,7 +2156,7 @@ ${attributionInstructions} private async cleanupSession(): Promise { if (!this.session) return; - this.logger.info("Cleaning up session"); + this.logger.debug("Cleaning up session"); try { await this.captureTreeState();