From fd5701ddb06bf7c2226c56713bed59ab29f38a31 Mon Sep 17 00:00:00 2001 From: Nate Date: Mon, 19 Jan 2026 13:41:32 -0800 Subject: [PATCH 1/5] fix: correctly set isComplete in metadata --- extensions/cli/src/commands/serve.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/cli/src/commands/serve.ts b/extensions/cli/src/commands/serve.ts index f0a41469b3f..b6e2ee3f6ea 100644 --- a/extensions/cli/src/commands/serve.ts +++ b/extensions/cli/src/commands/serve.ts @@ -526,10 +526,10 @@ export async function serve(prompt?: string, options: ServeOptions = {}) { state.lastActivity = Date.now(); - // Update metadata after successful agent turn + // Update metadata after successful agent turn (mark as complete for this turn) try { const history = services.chatHistory?.getHistory(); - await updateAgentMetadata(history); + await updateAgentMetadata({ history, isComplete: true }); } catch (metadataErr) { // Non-critical: log but don't fail the agent execution logger.debug( From 65a1e1500b112de4e24364eff890bd9b4fcac895 Mon Sep 17 00:00:00 2001 From: Nate Date: Sun, 25 Jan 2026 15:47:56 -0800 Subject: [PATCH 2/5] fix: Set isComplete metadata when agents finish execution Fixes issue where agent sessions stay stuck in "running" state forever because the isComplete metadata field is never set. Changes: - Status tool now calls updateAgentMetadata({ isComplete: true }) when status is "DONE" or "FAILED" - reportFailure tool now marks agent as complete before reporting failure - exit tool now marks agent as complete before exiting - Added debug logging to track inactivity timeout progress and lastActivity updates This ensures the UI can properly detect when an agent has finished and display the correct state instead of showing "running" indefinitely. Co-Authored-By: Claude Sonnet 4.5 --- extensions/cli/src/commands/serve.ts | 29 ++++++++++++++++++++++- extensions/cli/src/tools/exit.ts | 13 +++++++++- extensions/cli/src/tools/reportFailure.ts | 13 ++++++++++ extensions/cli/src/tools/status.ts | 19 +++++++++++++++ 4 files changed, 72 insertions(+), 2 deletions(-) diff --git a/extensions/cli/src/commands/serve.ts b/extensions/cli/src/commands/serve.ts index f0a41469b3f..8f5f152d1ae 100644 --- a/extensions/cli/src/commands/serve.ts +++ b/extensions/cli/src/commands/serve.ts @@ -430,6 +430,9 @@ export async function serve(prompt?: string, options: ServeOptions = {}) { `\nServer will shut down after ${timeoutSeconds} seconds of inactivity`, ), ); + logger.info( + `Inactivity timeout configured: ${timeoutSeconds}s (${timeoutMs}ms)`, + ); // Run environment install script after server startup runEnvironmentInstallSafe(); @@ -497,6 +500,9 @@ export async function serve(prompt?: string, options: ServeOptions = {}) { const userMessage = queuedMessage.message; state.isProcessing = true; state.lastActivity = Date.now(); + logger.debug( + `Updated lastActivity: processing message "${userMessage.substring(0, 50)}${userMessage.length > 50 ? "..." : ""}"`, + ); processedMessage = true; // Add user message via ChatHistoryService (single source of truth) @@ -525,6 +531,7 @@ export async function serve(prompt?: string, options: ServeOptions = {}) { // No direct persistence here; ChatHistoryService handles persistence when appropriate state.lastActivity = Date.now(); + logger.debug("Updated lastActivity: after successful agent turn"); // Update metadata after successful agent turn try { @@ -584,13 +591,32 @@ export async function serve(prompt?: string, options: ServeOptions = {}) { } // Check for inactivity and shutdown + let inactivityCheckCount = 0; inactivityChecker = setInterval(() => { - if (!state.isProcessing && Date.now() - state.lastActivity > timeoutMs) { + inactivityCheckCount++; + const now = Date.now(); + const timeSinceLastActivity = now - state.lastActivity; + const isProcessing = state.isProcessing; + const shouldShutdown = !isProcessing && timeSinceLastActivity > timeoutMs; + + // Log every 30 seconds to track timeout progress + if (inactivityCheckCount % 30 === 0) { + logger.debug( + `Inactivity check #${inactivityCheckCount}: processing=${isProcessing}, ` + + `timeSinceLastActivity=${Math.floor(timeSinceLastActivity / 1000)}s, ` + + `timeout=${timeoutSeconds}s, shouldShutdown=${shouldShutdown}`, + ); + } + + if (shouldShutdown) { console.log( chalk.yellow( `\nShutting down due to ${timeoutSeconds} seconds of inactivity`, ), ); + logger.info( + `Inactivity timeout triggered after ${inactivityCheckCount} checks`, + ); state.serverRunning = false; stopStorageSync(); server.close(async () => { @@ -600,6 +626,7 @@ export async function serve(prompt?: string, options: ServeOptions = {}) { try { const history = services.chatHistory?.getHistory(); await updateAgentMetadata({ history, isComplete: true }); + logger.info("Marked agent as complete due to inactivity timeout"); } catch (err) { logger.debug("Failed to update metadata (non-critical)", err as any); } diff --git a/extensions/cli/src/tools/exit.ts b/extensions/cli/src/tools/exit.ts index a6d84bdc3ee..ebb963af48b 100644 --- a/extensions/cli/src/tools/exit.ts +++ b/extensions/cli/src/tools/exit.ts @@ -12,7 +12,18 @@ export const exitTool: Tool = { readonly: false, isBuiltIn: true, run: async (): Promise => { - const { gracefulExit } = await import("../util/exit.js"); + const { gracefulExit, updateAgentMetadata } = await import( + "../util/exit.js" + ); + + // Mark agent as complete before exiting + try { + await updateAgentMetadata({ isComplete: true }); + } catch (err) { + // Non-critical: log but don't block exit + console.debug("Failed to update completion metadata (non-critical)", err); + } + await gracefulExit(1); return ""; }, diff --git a/extensions/cli/src/tools/reportFailure.ts b/extensions/cli/src/tools/reportFailure.ts index 1e0fede2e65..a2d56fe3304 100644 --- a/extensions/cli/src/tools/reportFailure.ts +++ b/extensions/cli/src/tools/reportFailure.ts @@ -6,6 +6,7 @@ import { AuthenticationRequiredError, post, } from "../util/apiClient.js"; +import { updateAgentMetadata } from "../util/exit.js"; import { logger } from "../util/logger.js"; import { Tool } from "./types.js"; @@ -74,6 +75,18 @@ export const reportFailureTool: Tool = { errorMessage: trimmedMessage, }); + // Mark agent as complete since it failed + try { + await updateAgentMetadata({ isComplete: true }); + logger.debug("Marked agent as complete due to failure"); + } catch (metadataErr) { + // Non-critical: log but don't fail the failure report + logger.debug( + "Failed to update completion metadata (non-critical)", + metadataErr as any, + ); + } + logger.info(`Failure reported: ${trimmedMessage}`); return "Failure reported to user."; } catch (error) { diff --git a/extensions/cli/src/tools/status.ts b/extensions/cli/src/tools/status.ts index c75eec837f6..c545a8d581d 100644 --- a/extensions/cli/src/tools/status.ts +++ b/extensions/cli/src/tools/status.ts @@ -5,6 +5,7 @@ import { AuthenticationRequiredError, post, } from "../util/apiClient.js"; +import { updateAgentMetadata } from "../util/exit.js"; import { logger } from "../util/logger.js"; import { Tool } from "./types.js"; @@ -62,6 +63,24 @@ You should use this tool to notify the user whenever the state of your work chan await post(`agents/${agentId}/status`, { status: args.status }); logger.info(`Status: ${args.status}`); + + // If status is DONE or FAILED, mark agent as complete + const normalizedStatus = args.status.toUpperCase(); + if (normalizedStatus === "DONE" || normalizedStatus === "FAILED") { + try { + await updateAgentMetadata({ isComplete: true }); + logger.debug( + `Marked agent as complete due to status: ${args.status}`, + ); + } catch (metadataErr) { + // Non-critical: log but don't fail the status update + logger.debug( + "Failed to update completion metadata (non-critical)", + metadataErr as any, + ); + } + } + return `Status set: ${args.status}`; } catch (error) { if (error instanceof ContinueError) { From d865b5c4b6069cd799fab3c41ea6d91af9948522 Mon Sep 17 00:00:00 2001 From: Nate Date: Sun, 25 Jan 2026 15:57:19 -0800 Subject: [PATCH 3/5] Remove debug logging from serve.ts --- extensions/cli/src/commands/serve.ts | 29 +--------------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/extensions/cli/src/commands/serve.ts b/extensions/cli/src/commands/serve.ts index 8f5f152d1ae..f0a41469b3f 100644 --- a/extensions/cli/src/commands/serve.ts +++ b/extensions/cli/src/commands/serve.ts @@ -430,9 +430,6 @@ export async function serve(prompt?: string, options: ServeOptions = {}) { `\nServer will shut down after ${timeoutSeconds} seconds of inactivity`, ), ); - logger.info( - `Inactivity timeout configured: ${timeoutSeconds}s (${timeoutMs}ms)`, - ); // Run environment install script after server startup runEnvironmentInstallSafe(); @@ -500,9 +497,6 @@ export async function serve(prompt?: string, options: ServeOptions = {}) { const userMessage = queuedMessage.message; state.isProcessing = true; state.lastActivity = Date.now(); - logger.debug( - `Updated lastActivity: processing message "${userMessage.substring(0, 50)}${userMessage.length > 50 ? "..." : ""}"`, - ); processedMessage = true; // Add user message via ChatHistoryService (single source of truth) @@ -531,7 +525,6 @@ export async function serve(prompt?: string, options: ServeOptions = {}) { // No direct persistence here; ChatHistoryService handles persistence when appropriate state.lastActivity = Date.now(); - logger.debug("Updated lastActivity: after successful agent turn"); // Update metadata after successful agent turn try { @@ -591,32 +584,13 @@ export async function serve(prompt?: string, options: ServeOptions = {}) { } // Check for inactivity and shutdown - let inactivityCheckCount = 0; inactivityChecker = setInterval(() => { - inactivityCheckCount++; - const now = Date.now(); - const timeSinceLastActivity = now - state.lastActivity; - const isProcessing = state.isProcessing; - const shouldShutdown = !isProcessing && timeSinceLastActivity > timeoutMs; - - // Log every 30 seconds to track timeout progress - if (inactivityCheckCount % 30 === 0) { - logger.debug( - `Inactivity check #${inactivityCheckCount}: processing=${isProcessing}, ` + - `timeSinceLastActivity=${Math.floor(timeSinceLastActivity / 1000)}s, ` + - `timeout=${timeoutSeconds}s, shouldShutdown=${shouldShutdown}`, - ); - } - - if (shouldShutdown) { + if (!state.isProcessing && Date.now() - state.lastActivity > timeoutMs) { console.log( chalk.yellow( `\nShutting down due to ${timeoutSeconds} seconds of inactivity`, ), ); - logger.info( - `Inactivity timeout triggered after ${inactivityCheckCount} checks`, - ); state.serverRunning = false; stopStorageSync(); server.close(async () => { @@ -626,7 +600,6 @@ export async function serve(prompt?: string, options: ServeOptions = {}) { try { const history = services.chatHistory?.getHistory(); await updateAgentMetadata({ history, isComplete: true }); - logger.info("Marked agent as complete due to inactivity timeout"); } catch (err) { logger.debug("Failed to update metadata (non-critical)", err as any); } From b65f44027a9c36d02e930ed2be41bac274ceb312 Mon Sep 17 00:00:00 2001 From: Nate Date: Sun, 25 Jan 2026 16:07:54 -0800 Subject: [PATCH 4/5] fix: Replace console.debug with logger in exit tool Addresses code review feedback to use shared logger instead of console.debug --- extensions/cli/src/tools/exit.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/extensions/cli/src/tools/exit.ts b/extensions/cli/src/tools/exit.ts index ebb963af48b..987b9522ecf 100644 --- a/extensions/cli/src/tools/exit.ts +++ b/extensions/cli/src/tools/exit.ts @@ -1,3 +1,5 @@ +import { logger } from "../util/logger.js"; + import { Tool } from "./types.js"; export const exitTool: Tool = { @@ -21,7 +23,10 @@ export const exitTool: Tool = { await updateAgentMetadata({ isComplete: true }); } catch (err) { // Non-critical: log but don't block exit - console.debug("Failed to update completion metadata (non-critical)", err); + logger.debug( + "Failed to update completion metadata (non-critical)", + err as any, + ); } await gracefulExit(1); From ebc81a62287dc5dfeaf27f54d2f0df7b94e3ae9d Mon Sep 17 00:00:00 2001 From: Nate Date: Sun, 25 Jan 2026 20:19:06 -0800 Subject: [PATCH 5/5] Fix: Set isComplete=true after agent turn ends without tool calls When an agent completes a turn with a final response (no tool calls), we now correctly set isComplete=true in the metadata. Previously, isComplete was only set when Exit, ReportFailure, or Status tools were called, or when the server shut down. This caused agents that gave final responses to remain in 'running' state indefinitely. The fix checks the last message in history after each turn - if it's from the assistant and has no tool_calls, the agent is marked as complete. Co-Authored-By: Claude Sonnet 4.5 --- extensions/cli/src/commands/serve.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/extensions/cli/src/commands/serve.ts b/extensions/cli/src/commands/serve.ts index f0a41469b3f..cefe2e6dd9b 100644 --- a/extensions/cli/src/commands/serve.ts +++ b/extensions/cli/src/commands/serve.ts @@ -529,7 +529,19 @@ export async function serve(prompt?: string, options: ServeOptions = {}) { // Update metadata after successful agent turn try { const history = services.chatHistory?.getHistory(); - await updateAgentMetadata(history); + + // Check if the agent should be marked as complete + // The agent is complete if the last message is from the assistant and has no tool calls + let isComplete = false; + if (history && history.length > 0) { + const lastItem = history[history.length - 1]; + if (lastItem?.message?.role === "assistant") { + const toolCalls = (lastItem.message as any).tool_calls; + isComplete = !toolCalls || toolCalls.length === 0; + } + } + + await updateAgentMetadata({ history, isComplete }); } catch (metadataErr) { // Non-critical: log but don't fail the agent execution logger.debug(