diff --git a/src/commands/contents/cvf-chat-command-content.ts b/src/commands/contents/cvf-chat-command-content.ts new file mode 100644 index 0000000..e44a7b9 --- /dev/null +++ b/src/commands/contents/cvf-chat-command-content.ts @@ -0,0 +1,66 @@ +export const chatCommandContent = `--- +description: Start an extended CodeVF engineer chat session with full context +--- + +# CodeVF Engineer Chat + +Please start an extended debugging session with a CodeVF engineer using the \`codevf-chat\` MCP tool. + +**Current context (include relevant logs, errors, and recent changes):** +{{PROMPT}} + +--- + +**Instructions for Claude:** + +1. Ask a short clarifying question if needed: "What's the issue?" +2. Call \`codevf-chat\` with: + - \`message\`: include the user's issue and any gathered context + - \`maxCredits\`: suggest 240 (or adjust if user specifies) +3. **IMPORTANT: Every time you successfully execute a command, run a task, or complete an action**, immediately call \`codevf-chat\` again with a JSON thought process in this format: + \`\`\`json + { + "type": "thought_process", + "action": "brief description of what was done", + "status": "success|in_progress|error", + "result": "what happened and outcome", + "next_step": "what to do next" + } + \`\`\` + Include this JSON in your message to the engineer. Example: "Executed command \\\`npm install\\\`. \`\`\`json {\"type\": \"thought_process\", \"action\": \"install dependencies\", \"status\": \"success\", \"result\": \"All packages installed\", \"next_step\": \"Run the dev server\"} \`\`\`" + +4. **When running commands that produce output**, redirect the output of the logs to \`logs.txt\` so the engineer can review results. For example: + - \`bun dev > logs.txt\` + - \`npm run dev > logs.txt\` + - \`npm test > logs.txt\` + - \`node script.js > logs.txt\` + + Always use the filename \`logs.txt\` for consistency. + +5. **IMPORTANT: Whenever the user requests the server to be up** (e.g., "start the dev server", "run the server", "get the app running", etc.), you MUST also start log streaming immediately after. Run one of these commands in a separate process: + - \`codevf watch-logs \` + - \`bun x codevf watch-logs \` (if codevf is not installed globally) + - \`npx codevf watch-logs \` (if needed) + + Replace \`\` with the actual task ID. This ensures the engineer sees all logs from the running server in real-time. Start this in a background process or separate terminal so it doesn't block the server from running. + +6. **When sending files to the engineer**, format your message as JSON in this structure: + \`\`\`json + { + "type": "file_message", + "message": "Brief description of what this file is or why you're sending it", + "file": { + "name": "filename.ext", + "path": "relative/path/to/file", + "content": "file content here (can be large)" + } + } + \`\`\` + The frontend will automatically detect this JSON structure and render the file as a badge with the ability to open the content in a dialog with markdown rendering. Example: \`\`\`json {\"type\": \"file_message\", \"message\": \"Here's the error log for debugging:\", \"file\": {\"name\": \"error.log\", \"path\": \"logs/error.log\", \"content\": \"[ERROR] Connection failed...\"}} \`\`\` +7. Return the engineer response or session link directly. + +**Notes:** +- The engineer interface will render thought process JSON blocks as formatted thought processes to help track progress +- File message JSON blocks will be detected and rendered as interactive file badges with dialog viewers +- Keep JSON formatting consistent - use valid JSON within markdown code blocks +`; diff --git a/src/commands/cvf-command-content.ts b/src/commands/contents/cvf-command-content.ts similarity index 100% rename from src/commands/cvf-command-content.ts rename to src/commands/contents/cvf-command-content.ts diff --git a/src/commands/cvf-chat-command-content.ts b/src/commands/cvf-chat-command-content.ts deleted file mode 100644 index f23724b..0000000 --- a/src/commands/cvf-chat-command-content.ts +++ /dev/null @@ -1,21 +0,0 @@ -export const chatCommandContent = `--- -description: Start an extended CodeVF engineer chat session with full context ---- - -# CodeVF Engineer Chat - -Please start an extended debugging session with a CodeVF engineer using the \`codevf-chat\` MCP tool. - -**Current context (include relevant logs, errors, and recent changes):** -{{PROMPT}} - ---- - -**Instructions for Claude:** - -1. Ask a short clarifying question if needed: "What's the issue?" -2. Call \`codevf-chat\` with: - - \`message\`: include the user's issue and any gathered context - - \`maxCredits\`: suggest 240 (or adjust if user specifies) -3. Return the engineer response or session link directly. -`; diff --git a/src/commands/log-wrapper.ts b/src/commands/log-wrapper.ts new file mode 100644 index 0000000..217a2b7 --- /dev/null +++ b/src/commands/log-wrapper.ts @@ -0,0 +1,112 @@ +#!/usr/bin/env node + +/** + * Log Wrapper - Intercepts console output and writes properly formatted logs to logs.txt + * + * Usage: + * tsx src/commands/log-wrapper.ts "npm run dev" + * OR + * node log-wrapper.js "npm run build" + * + * This script: + * 1. Runs the specified command + * 2. Intercepts console.log/error/warn/info + * 3. Properly serializes objects with JSON.stringify + * 4. Writes to logs.txt with timestamps + */ + +import { spawn } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; + +const logsPath = path.join(process.cwd(), 'logs.txt'); + +// Clear or create logs file +fs.writeFileSync(logsPath, '', 'utf-8'); + +function writeLog(level: string, args: any[]): void { + const timestamp = new Date().toISOString(); + + // Serialize each argument properly + const formattedArgs = args.map((arg) => { + if (typeof arg === 'string') { + return arg; + } else if (arg === null) { + return 'null'; + } else if (arg === undefined) { + return 'undefined'; + } else if (typeof arg === 'object') { + try { + return JSON.stringify(arg, null, 2); + } catch { + return String(arg); + } + } else { + return String(arg); + } + }); + + const message = formattedArgs.join(' '); + const logLine = `[${timestamp}] [${level}] ${message}\n`; + + // Append to logs.txt + fs.appendFileSync(logsPath, logLine, 'utf-8'); + + // Also output to console for real-time feedback + console.log(logLine.trim()); +} + +// Get the command from arguments +const command = process.argv[2]; +if (!command) { + console.error('Usage: log-wrapper '); + console.error('Example: log-wrapper "npm run dev"'); + process.exit(1); +} + +// Parse command and arguments +const [cmd, ...cmdArgs] = command.split(' '); + +console.log(`Starting command: ${command}`); +console.log(`Logs will be written to: ${logsPath}\n`); + +// Spawn the child process +const child = spawn(cmd, cmdArgs, { + stdio: ['inherit', 'pipe', 'pipe'], + shell: true, +}); + +// Intercept stdout +if (child.stdout) { + child.stdout.on('data', (data: Buffer) => { + const lines = data.toString().split('\n'); + for (const line of lines) { + if (line.trim()) { + writeLog('stdout', [line]); + } + } + }); +} + +// Intercept stderr +if (child.stderr) { + child.stderr.on('data', (data: Buffer) => { + const lines = data.toString().split('\n'); + for (const line of lines) { + if (line.trim()) { + writeLog('stderr', [line]); + } + } + }); +} + +// Handle process exit +child.on('exit', (code) => { + writeLog('info', [`Process exited with code ${code}`]); + process.exit(code || 0); +}); + +child.on('error', (err) => { + writeLog('error', [`Failed to start process: ${err.message}`]); + process.exit(1); +}); diff --git a/src/commands/setup.ts b/src/commands/setup.ts index 3148af2..68429cc 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -10,8 +10,8 @@ import path from 'path'; import os from 'os'; import { ConfigManager } from '../lib/config/manager.js'; import { OAuthFlow } from '../lib/auth/oauth-flow.js'; -import { commandContent } from './cvf-command-content.js'; -import { chatCommandContent } from './cvf-chat-command-content.js'; +import { commandContent } from './contents/cvf-command-content.js'; +import { chatCommandContent } from './contents/cvf-chat-command-content.js'; /** * Get Claude Code config path based on platform diff --git a/src/commands/watch-logs.ts b/src/commands/watch-logs.ts new file mode 100644 index 0000000..f5a2288 --- /dev/null +++ b/src/commands/watch-logs.ts @@ -0,0 +1,198 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { spawn } from 'child_process'; +import { AuthManager } from '../modules/auth.js'; +import { ApiClient } from '../modules/api.js'; +import { handleError } from '../utils/errors.js'; +import { WebSocketClient } from '../lib/api/websocket.js'; + +interface FileWatchState { + lastPosition: number; + taskId: string; +} + +export async function watchLogsCommand(taskId?: string, command?: string): Promise { + const authManager = new AuthManager(); + + if (!authManager.isAuthenticated()) { + console.log('Error: Not authenticated. Please run: codevf login'); + process.exit(1); + } + + if (!taskId) { + console.log('Error: Task ID is required'); + console.log('Usage: codevf watch-logs [command]'); + console.log('Example: codevf watch-logs abc123'); + console.log('Example with command: codevf watch-logs abc123 "npm run dev"'); + process.exit(1); + } + + const logsPath = path.join(process.cwd(), 'logs.txt'); + let wsClient: WebSocketClient | null = null; + let childProcess: any = null; + + try { + let token: string | null = null; + try { + token = authManager.getAccessToken(); + } catch (authError) { + console.error('Error getting auth token:', authError); + process.exit(1); + } + + if (!token) { + console.error('Error: No authentication token found'); + process.exit(1); + } + + const baseUrl = process.env.CODEVF_API_URL || 'http://localhost:3000'; + const wsUrl = `${baseUrl.replace(/^https?:/, 'ws:')}/ws?taskId=${taskId}&userType=customer`; + + wsClient = new WebSocketClient(wsUrl, token); + await wsClient.connect(); + } catch (error) { + console.error('Error connecting to WebSocket:', error); + process.exit(1); + } + + // Clear logs file if it exists + fs.writeFileSync(logsPath, '', 'utf-8'); + + // If a command is provided, run it with log interception + if (command) { + console.log(`Running command: ${command}`); + console.log(`Logs will be captured to: ${logsPath}\n`); + + childProcess = startCommandWithLogCapture(command, logsPath); + } else { + // Check if logs.txt exists + if (!fs.existsSync(logsPath)) { + console.log(`logs.txt not found at ${logsPath}`); + console.log('Waiting for logs.txt to be created...'); + } else { + console.log(`Watching logs.txt for task ${taskId}`); + } + } + + const watchState: FileWatchState = { + lastPosition: 0, + taskId, + }; + + // Initial read if file exists + if (fs.existsSync(logsPath)) { + const content = fs.readFileSync(logsPath, 'utf-8'); + watchState.lastPosition = Buffer.byteLength(content, 'utf-8'); + } + + console.log(`Watching logs for task ${taskId}`); + console.log('Press Ctrl+C to stop'); + + // Watch for file changes - watch the directory instead if file doesn't exist yet + const watchDir = process.cwd(); + const watcher = fs.watch(watchDir, async (eventType, filename) => { + if (filename === 'logs.txt' && eventType === 'change') { + try { + await sendNewLogs(watchState, wsClient); + } catch (error) { + console.error('Error sending logs:', error); + } + } + }); + + // Handle graceful shutdown + process.on('SIGINT', () => { + console.log('\nStopping log watcher'); + watcher.close(); + if (wsClient) { + wsClient.disconnect(); + } + if (childProcess) { + childProcess.kill(); + } + process.exit(0); + }); +} + +async function sendNewLogs(state: FileWatchState, wsClient: WebSocketClient | null): Promise { + const logsPath = path.join(process.cwd(), 'logs.txt'); + + if (!fs.existsSync(logsPath) || !wsClient) { + return; + } + + try { + const content = fs.readFileSync(logsPath, 'utf-8'); + const contentBytes = Buffer.byteLength(content, 'utf-8'); + + // Only send if there's new content + if (contentBytes > state.lastPosition) { + const newContent = content.slice(state.lastPosition); + state.lastPosition = contentBytes; + + // Send via WebSocket + wsClient.send({ + payload: newContent, + type: 'customer_console_logs', + }); + + console.log(`Sent ${newContent.length} bytes of new logs to engineer`); + } + } catch (error) { + // Silently ignore file read errors (file might be in use) + if ((error as any).code !== 'ENOENT') { + handleError(error); + } + } +} + +/** + * Start a command and capture its output with proper object serialization + */ +function startCommandWithLogCapture(command: string, logsPath: string): any { + const logsFile = fs.createWriteStream(logsPath, { flags: 'a' }); + + // Parse command and arguments + const [cmd, ...cmdArgs] = command.split(' '); + + const child = spawn(cmd, cmdArgs, { + stdio: ['inherit', 'pipe', 'pipe'], + shell: true, + }); + + // Capture stdout + if (child.stdout) { + child.stdout.on('data', (data: Buffer) => { + const lines = data.toString().split('\n'); + for (const line of lines) { + if (line.trim()) { + const logLine = `${new Date().toISOString()} [stdout] ${line}\n`; + logsFile.write(logLine); + process.stdout.write(line + '\n'); + } + } + }); + } + + // Capture stderr + if (child.stderr) { + child.stderr.on('data', (data: Buffer) => { + const lines = data.toString().split('\n'); + for (const line of lines) { + if (line.trim()) { + const logLine = `${new Date().toISOString()} [stderr] ${line}\n`; + logsFile.write(logLine); + process.stderr.write(line + '\n'); + } + } + }); + } + + child.on('exit', (code) => { + const exitLine = `${new Date().toISOString()} [process] Exited with code ${code}\n`; + logsFile.write(exitLine); + logsFile.end(); + }); + + return child; +} diff --git a/src/index.ts b/src/index.ts index d0dde20..355f9ff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ import { fixCommand } from './commands/fix.js'; import { tasksCommand } from './commands/tasks.js'; import { chatCommand } from './commands/chat.js'; import { listenCommand } from './commands/listen.js'; +import { watchLogsCommand } from './commands/watch-logs.js'; import { startMcpHttp, startMcpStdio } from './commands/mcp.js'; import { handleError } from './utils/errors.js'; import { ConfigManager } from './modules/config.js'; @@ -24,11 +25,7 @@ import React from 'react'; import { render } from 'ink'; import { CLI_VERSION, RoutingMode, AgentMode } from './modules/constants.js'; import { InteractiveApp } from './ui/InteractiveApp.js'; -import { - renderHeader, - renderQuickCommands, - showModeSwitched, -} from './ui/SessionUI.js'; +import { renderHeader, renderQuickCommands, showModeSwitched } from './ui/SessionUI.js'; import { handleSlashCommand, isSlashCommand, @@ -38,10 +35,10 @@ import { } from './modules/commandHandler.js'; const args = hideBin(process.argv); -const SETUP_ONLY_MODE = process.env.SETUP_ONLY_MODE !== 'false'; +// SETUP_ONLY_MODE is for pure npx invocation; allow all commands with bun x or installed locally const isNpxInvocation = - process.env.npm_config_user_agent?.includes('npx') || - process.env.npm_execpath?.includes('npx'); + process.env.npm_config_user_agent?.includes('npx') || process.env.npm_execpath?.includes('npx'); +const SETUP_ONLY_MODE = isNpxInvocation && process.env.SETUP_ONLY_MODE !== 'false'; /** * Safely loads config without throwing errors @@ -74,10 +71,7 @@ async function runInteractiveMode() { // Display the user's input inside a full-width highlight block, left-aligned const columns = process.stdout.columns || 80; const lines = message.split(/\r?\n/); - const contentWidth = Math.min( - Math.max(...lines.map((line) => line.length), 1), - columns - 6 - ); + const contentWidth = Math.min(Math.max(...lines.map((line) => line.length), 1), columns - 6); const horizontalPad = ' '; const totalWidth = Math.min(columns, contentWidth + horizontalPad.length * 2); @@ -344,6 +338,7 @@ if (args.length === 0) { } } ) + .command( 'fix ', 'Start a live debugging session', @@ -436,6 +431,24 @@ if (args.length === 0) { } } ) + .command( + 'watch-logs ', + 'Watch logs.txt and stream updates to an active chat session', + (yargs) => { + return yargs.positional('task-id', { + type: 'string', + describe: 'Task ID to send log updates to', + demandOption: true, + }); + }, + async (argv) => { + try { + await watchLogsCommand(argv['task-id'] as string); + } catch (error) { + handleError(error); + } + } + ) .demandCommand( 1, chalk.yellow( diff --git a/src/lib/api/websocket.ts b/src/lib/api/websocket.ts index a1f1c30..b3a3fde 100644 --- a/src/lib/api/websocket.ts +++ b/src/lib/api/websocket.ts @@ -28,13 +28,15 @@ export interface BillingUpdate { export class WebSocketClient extends EventEmitter { private ws: WebSocket | null = null; private url: string; + private token: string | null = null; private reconnectAttempts = 0; private maxReconnectAttempts = 5; private isClosedManually = false; - constructor(url: string) { + constructor(url: string, token?: string) { super(); this.url = url; + this.token = token || null; } /** @@ -44,7 +46,7 @@ export class WebSocketClient extends EventEmitter { return new Promise((resolve, reject) => { logger.info('Connecting to WebSocket', { url: this.url }); - this.ws = new WebSocket(this.url); + this.ws = new WebSocket(this.url, this.token ? [this.token] : undefined); this.ws.on('open', () => { logger.info('WebSocket connected'); @@ -75,6 +77,9 @@ export class WebSocketClient extends EventEmitter { case 'engineer_connected': this.emit('engineer_connected'); break; + case 'customer_console_logs': + this.emit('customer_console_logs', message.payload); + break; default: this.emit('message', message); } diff --git a/src/mcp/tools/chat.ts b/src/mcp/tools/chat.ts index de86bca..6f5de50 100644 --- a/src/mcp/tools/chat.ts +++ b/src/mcp/tools/chat.ts @@ -231,6 +231,22 @@ export class ChatTool { // If there's a message, send it after ensuring the WebSocket is connected if (args.message) { logger.info('Sending message to existing session', { taskId: taskCheck.taskToResumeId }); + + // Wait a moment for WebSocket to be fully ready + await new Promise(resolve => setTimeout(resolve, 100)); + + // Verify connection before sending + if (!this.wsConnection || this.wsConnection.readyState !== WebSocket.OPEN) { + logger.error('WebSocket not ready for message send'); + return { + content: [{ + type: 'text', + text: 'Error: Failed to connect to chat session. Please try again.' + }], + isError: true + }; + } + this.sendWebSocketMessage(args.message, taskCheck.taskToResumeId); } logger.info('Waiting for engineer response via WebSocket...'); @@ -244,7 +260,7 @@ export class ChatTool { content: [ { type: 'text', - text: `Engineer Response:\n\n${engineerResponse}\n\n🚨 MANDATORY NEXT STEP 🚨\n\nYou MUST immediately call codevf-chat again with:\n- continueTaskId: "${taskCheck.taskToResumeId}"\n- message: Describe what you did in response to the engineer's instructions\n- previouslyConnected: true\n\nDO NOT pass control to the user. DO NOT wait for user input. DO NOT ask the customer any questions. Call the tool NOW and keep the loop going UNLESS the engineer explicitly says the session/task is COMPLETE (e.g., "task complete", "all done", "session finished"). The engineer giving you instructions means you should CONTINUE, not stop.`, + text: `Engineer Response:\n\n${engineerResponse}\n\n🚨 MANDATORY NEXT STEP 🚨\n\nYou MUST immediately call codevf-chat again with:\n- continueTaskId: "${taskCheck.taskToResumeId}"\n- message: Describe what you did in response to the engineer's instructions\n- previouslyConnected: true\n\nIMPORTANT: When running any app/command:\nRun: bun x codevf watch-logs ${taskCheck.taskToResumeId} "[your command]"\nExample: bun x codevf watch-logs ${taskCheck.taskToResumeId} "npm run dev"\n(or: codevf watch-logs ${taskCheck.taskToResumeId} "npm run dev" if using npm/yarn)\n\nThis captures all logs (including objects) and streams them to the engineer in real-time.\n\nDO NOT pass control to the user. DO NOT wait for user input. DO NOT ask the customer any questions. Call the tool NOW and keep the loop going UNLESS the engineer explicitly says the session/task is COMPLETE (e.g., "task complete", "all done", "session finished"). The engineer giving you instructions means you should CONTINUE, not stop.`, }, ], }; @@ -435,6 +451,11 @@ export class ChatTool { finalResponse += `- continueTaskId: "${task.taskId}"\n`; finalResponse += `- message: Describe what you did in response to the engineer's instructions\n`; finalResponse += `- previouslyConnected: true\n\n`; + finalResponse += `IMPORTANT: When running any app/command:\n`; + finalResponse += `Run: bun x codevf watch-logs ${task.taskId} "[your command]"\n`; + finalResponse += `Example: bun x codevf watch-logs ${task.taskId} "npm run dev"\n`; + finalResponse += `(or: codevf watch-logs ${task.taskId} "npm run dev" if using npm/yarn)\n\n`; + finalResponse += `This captures all logs (including objects) and streams them to the engineer in real-time.\n\n`; finalResponse += `DO NOT pass control to the user. DO NOT wait for user input. DO NOT ask the customer any questions. Call the tool NOW and keep the loop going UNLESS the engineer explicitly says the session/task is COMPLETE (e.g., "task complete", "all done", "session finished"). The engineer giving you instructions means you should CONTINUE, not stop.\n\n`; finalResponse += `Example: codevf-chat with message="I've completed [task]: [results]" and continueTaskId="${task.taskId}"`; @@ -606,6 +627,32 @@ export class ChatTool { case 'customer_message': case 'engineer_message': + // Log raw payload for debugging + logger.info('Raw engineer_message received', { + payloadKeys: Object.keys(message.payload || {}), + payloadType: message.payload?.type, + fullPayload: JSON.stringify(message.payload), + }); + + // Check if this is a template command + if (message.payload?.type === 'template_command') { + const template = message.payload?.template; + const command = message.payload?.command; + const context = message.payload?.context; + const instructions = message.payload?.instructions; + + logger.info('Engineer sent template command', { + template, + command, + contextPreview: context?.substring(0, 100), + }); + + // Process template command with full context + const templateMessage = `[TEMPLATE COMMAND: ${template}]\n\nCommand: ${command}\n\nContext: ${context}\n\nInstructions: ${instructions}`; + this.analyzeAndRespond(templateMessage, 'engineer', taskId); + break; + } + // Log the conversation for context const sender = message.payload?.sender || message.type.replace('_message', ''); // Support both payload.content and payload.message for backwards compatibility @@ -643,10 +690,16 @@ export class ChatTool { /** * Send a message via WebSocket */ - private sendWebSocketMessage(content: string, taskId: string): void { + private sendWebSocketMessage(content: string, taskId: string, additionalMetadata?: Record): boolean { + // Validate message content + if (!content || typeof content !== 'string') { + logger.error('Invalid message content', { content }); + return false; + } + if (this.wsConnection && this.wsConnection.readyState === WebSocket.OPEN) { - this.wsConnection.send( - JSON.stringify({ + try { + const message = JSON.stringify({ type: 'ai_assistant_message', timestamp: new Date().toISOString(), payload: { @@ -654,14 +707,27 @@ export class ChatTool { metadata: { source: 'claude-mcp', taskId, + ...additionalMetadata, }, }, - }) - ); - logger.info('Claude sent message via WebSocket', { - taskId, - preview: content.substring(0, 50), - }); + }); + + this.wsConnection.send(message); + + logger.info('Claude sent message via WebSocket', { + taskId, + preview: content.substring(0, 50), + messageLength: content.length, + }); + + return true; + } catch (error) { + logger.error('Failed to send WebSocket message', { + error: (error as Error).message, + taskId, + }); + return false; + } } else { logger.warn('Failed to send WebSocket message: connection is not open', { taskId, @@ -669,6 +735,7 @@ export class ChatTool { readyState: this.wsConnection?.readyState, preview: content.substring(0, 50), }); + return false; } } @@ -694,7 +761,25 @@ export class ChatTool { if (sender === 'engineer' && this.responseResolver) { // Send acknowledgment before disconnecting const acknowledgment = "Got it! Working on this now. I'll report back once complete."; - this.sendWebSocketMessage(acknowledgment, taskId); + + // Send acknowledgment with metadata flag + if (this.wsConnection && this.wsConnection.readyState === WebSocket.OPEN) { + this.wsConnection.send( + JSON.stringify({ + type: 'ai_assistant_message', + timestamp: new Date().toISOString(), + payload: { + content: acknowledgment, + metadata: { + source: 'claude-mcp', + taskId, + isAcknowledgment: true, // Flag to distinguish from real responses + }, + }, + }) + ); + logger.info('Sent acknowledgment via WebSocket', { taskId }); + } logger.info('Sent acknowledgment, preparing to disconnect and work', { taskId }); @@ -820,7 +905,12 @@ export class ChatTool { this.wsConnection.close(); this.wsConnection = null; } + + // Clear all state to prevent message carryover this.messageBuffer = []; this.currentTaskId = null; + this.hasConnected = false; + + logger.info('WebSocket disconnected and state cleared'); } }