Advertisement

← vibesdk

worker/agents/core/behaviors/base.ts

81.5 KB Download file
import { Connection } from 'agents';
import { 
    FileConceptType,
    FileOutputType,
    Blueprint,
    AgenticBlueprint,
    PhasicBlueprint,
} from '../../schemas';
import { ExecuteCommandsResponse, PreviewType, RuntimeError, StaticAnalysisResponse, TemplateDetails, TemplateFile } from '../../../services/sandbox/sandboxTypes';
import { BaseProjectState, AgenticState, FileState } from '../state';
import { AllIssues, AgentSummary, AgentInitArgs, BehaviorType, DeploymentTarget, ProjectType } from '../types';
import { WebSocketMessageResponses } from '../../constants';
import { ProjectSetupAssistant } from '../../assistants/projectsetup';
import { UserConversationProcessor, RenderToolCall } from '../../operations/UserConversationProcessor';
import { FileRegenerationOperation } from '../../operations/FileRegeneration';
// Database schema imports removed - using zero-storage OAuth flow
import { BaseSandboxService } from '../../../services/sandbox/BaseSandboxService';
import { getTemplateImportantFiles } from '../../../services/sandbox/utils';
import { createScratchTemplateDetails } from '../../utils/templates';
import { WebSocketMessageData, WebSocketMessageType } from '../../../api/websocketTypes';
import { AgentActionKey, InferenceContext, InferenceRuntimeOverrides, ModelConfig } from '../../inferutils/config.types';
import { ModelConfigService } from '../../../database/services/ModelConfigService';
import { fixProjectIssues } from '../../../services/code-fixer';
import { FastCodeFixerOperation } from '../../operations/PostPhaseCodeFixer';
import { looksLikeCommand, validateAndCleanBootstrapCommands } from '../../utils/common';
import { customizeTemplateFiles, generateBootstrapScript } from '../../utils/templateCustomizer';
import { AppService } from '../../../database';
import { RateLimitExceededError } from 'shared/types/errors';
import { ImageAttachment, type ProcessedImageAttachment } from '../../../types/image-attachment';
import { OperationOptions } from '../../operations/common';
import { ImageType, uploadImage, detectBlankScreenshot } from 'worker/utils/images';
import { ScreenshotSecurity } from 'worker/utils/screenshot-security';
import { DeepDebugResult } from '../types';
import { updatePackageJson } from '../../utils/packageSyncer';
import { ICodingAgent } from '../../services/interfaces/ICodingAgent';
import { SimpleCodeGenerationOperation } from '../../operations/SimpleCodeGeneration';
import { AgentComponent } from '../AgentComponent';
import type { AgentInfrastructure } from '../AgentCore';
import { GitVersionControl } from '../../git';
import { DeepDebuggerOperation } from '../../operations/DeepDebugger';
import type { DeepDebuggerInputs } from '../../operations/DeepDebugger';
import { generatePortToken } from 'worker/utils/cryptoUtils';
import { getProtocolForHost, resolvePreviewHost } from 'worker/utils/urls';
import { isDev } from 'worker/utils/envs';
import { InMemoryAnalyzer } from '../../../services/static-analysis';
import { getBrowserCaptureClient } from '../../../services/browser-capture/factory';
import type { BrowserConsoleCaptureResult } from '../../../services/browser-capture/types';

// Screenshot capture configuration
const SCREENSHOT_CONFIG = {
    PAGE_LOAD_TIMEOUT: 15000,    // 15s for page load
    WAIT_FOR_TIMEOUT: 2000,      // 2s additional wait after network idle
    MAX_RETRIES: 2,              // 2 retries = 3 total attempts
    RETRY_DELAY_BASE: 2000,      // 2s base delay between retries
    MIN_FILE_SIZE: 10000,        // 10KB minimum for valid screenshot
    MIN_ENTROPY: 2.0,            // Minimum entropy threshold
};

export interface BaseCodingOperations {
    regenerateFile: FileRegenerationOperation;
    fastCodeFixer: FastCodeFixerOperation;
    processUserMessage: UserConversationProcessor;
    simpleGenerateFiles: SimpleCodeGenerationOperation;
}

/**
 * Base class for all coding behaviors
 */
export abstract class BaseCodingBehavior<TState extends BaseProjectState> 
    extends AgentComponent<TState> implements ICodingAgent {
    protected static readonly MAX_COMMANDS_HISTORY = 10;

    protected projectSetupAssistant: ProjectSetupAssistant | undefined;

    protected templateDetailsCache: TemplateDetails | null = null;
    
    // In-memory storage for user-uploaded images (not persisted in DO state)
    protected pendingUserImages: ProcessedImageAttachment[] = []
    protected generationPromise: Promise<void> | null = null;
    protected currentAbortController?: AbortController;
    protected deepDebugPromise: Promise<{ transcript: string } | { error: string }> | null = null;
    protected deepDebugConversationId: string | null = null;

    protected staticAnalysisCache: StaticAnalysisResponse | null = null;

    private sandboxReadyPromise: Promise<void>;
    private resolveSandboxReady!: () => void;

    protected userModelConfigs?: Record<AgentActionKey, ModelConfig>;
    protected runtimeOverrides?: InferenceRuntimeOverrides;
    
    protected operations: BaseCodingOperations = {
        regenerateFile: new FileRegenerationOperation(),
        fastCodeFixer: new FastCodeFixerOperation(),
        processUserMessage: new UserConversationProcessor(),
        simpleGenerateFiles: new SimpleCodeGenerationOperation(),
    };

    getBehavior(): BehaviorType {
        return this.state.behaviorType;
    }

    protected isAgenticState(state: BaseProjectState): state is AgenticState {
        return state.behaviorType === 'agentic';
    }

    constructor(infrastructure: AgentInfrastructure<TState>, protected projectType: ProjectType) {
        super(infrastructure);

        this.sandboxReadyPromise = new Promise(resolve => { this.resolveSandboxReady = resolve; });
        if (this.state.sandboxInstanceId) {
            this.resolveSandboxReady();
        }

        this.setState({
            ...this.state,
            behaviorType: this.getBehavior(),
            projectType: this.projectType,
        });
    }

    protected async waitForSandboxReady(timeoutMs: number = 5000): Promise<boolean> {
        const ready = await Promise.race([
            this.sandboxReadyPromise.then(() => true),
            new Promise<false>(resolve => setTimeout(() => resolve(false), timeoutMs))
        ]);
        if (!ready) {
            this.logger.warn(`Sandbox not ready after ${timeoutMs}ms`);
        }
        return ready;
    }

    public async initialize(
        initArgs: AgentInitArgs,
        ..._args: unknown[]
    ): Promise<TState> {
        this.logger.info("Initializing agent");
        const { templateInfo } = initArgs;
        if (templateInfo) {
            this.templateDetailsCache = templateInfo.templateDetails;
            
            await this.ensureTemplateDetails();
        }

        // Reset the logg
        return this.state;
    }

    onStart(_props?: Record<string, unknown> | undefined): Promise<void> {
        return Promise.resolve();
    }

    protected async initializeAsync(): Promise<void> {
        try {
            const [, setupCommands] = await Promise.all([
                this.deployToSandbox(),
                this.getProjectSetupAssistant().generateSetupCommands(),
                this.generateReadme()
            ]);
            this.logger.info("Deployment to sandbox service and initial commands predictions completed successfully");
                await this.executeCommands(setupCommands.commands);
                this.logger.info("Initial commands executed successfully");
        } catch (error) {
            this.logger.error("Error during async initialization:", error);
            // throw error;
        }
    }
    onStateUpdate(_state: TState, _source: "server" | Connection) {}

    async ensureTemplateDetails() {
        // Skip fetching details for "scratch" baseline
        if (!this.templateDetailsCache) {
            if (this.state.templateName === 'scratch') {
                this.logger.info('Skipping template details fetch for scratch baseline');
                return;
            }
            this.logger.info(`Loading template details for: ${this.state.templateName}`);
            const results = await BaseSandboxService.getTemplateDetails(this.state.templateName);
            if (!results.success || !results.templateDetails) {
                throw new Error(`Failed to get template details for: ${this.state.templateName}`);
            }
            
            const templateDetails = results.templateDetails;
            
            const customizedAllFiles = { ...templateDetails.allFiles };
            
            this.logger.info('Customizing template files for older app');
            const customizedFiles = customizeTemplateFiles(
                templateDetails.allFiles,
                {
                    projectName: this.state.projectName,
                    commandsHistory: this.getBootstrapCommands()
                }
            );
            Object.assign(customizedAllFiles, customizedFiles);
            
            this.templateDetailsCache = {
                ...templateDetails,
                allFiles: customizedAllFiles
            };
            this.logger.info('Template details loaded and customized');

            // If renderMode == 'browser', we can deploy right away
            if (templateDetails.renderMode === 'browser') {
                await this.deployToSandbox();
            }
        }
        return this.templateDetailsCache;
    }

    public getTemplateDetails(): TemplateDetails {
        if (!this.templateDetailsCache) {
            // Synthesize a minimal scratch template when starting from scratch
            if (this.state.templateName === 'scratch') {
                this.templateDetailsCache = createScratchTemplateDetails();
                return this.templateDetailsCache;
            }
            this.ensureTemplateDetails();
            throw new Error('Template details not loaded. Call ensureTemplateDetails() first.');
        }
        return this.templateDetailsCache;
    }

    protected isPreviewable(): boolean {
        // If there are 'package.json', and 'wrangler.jsonc' files, then it is previewable
        return this.fileManager.fileExists('package.json') && (this.fileManager.fileExists('wrangler.jsonc') || this.fileManager.fileExists('wrangler.toml'));
    }

    /**
     * Update bootstrap script when commands history changes
     * Called after significant command executions
     */
    private async updateBootstrapScript(commandsHistory: string[]): Promise<void> {
        if (!commandsHistory || commandsHistory.length === 0) {
            return;
        }
        
        // Use only validated commands
        const bootstrapScript = generateBootstrapScript(
            this.state.projectName,
            commandsHistory
        );
        
        await this.fileManager.saveGeneratedFile(
            {
                filePath: '.bootstrap.js',
                fileContents: bootstrapScript,
                filePurpose: 'Updated bootstrap script for first-time clone setup'
            },
            'chore: Update bootstrap script with latest commands',
            true
        );
        
        this.logger.info('Updated bootstrap script with commands', {
            commandCount: commandsHistory.length,
            commands: commandsHistory
        });
    }

    getProjectSetupAssistant(): ProjectSetupAssistant {
        if (this.projectSetupAssistant === undefined) {
            this.projectSetupAssistant = new ProjectSetupAssistant({
                env: this.env,
                agentId: this.getAgentId(),
                query: this.state.query,
                blueprint: this.state.blueprint,
                template: this.getTemplateDetails(),
                inferenceContext: this.getInferenceContext()
            });
        }
        return this.projectSetupAssistant;
    }

    getSessionId() {
        return this.deploymentManager.getSessionId();
    }

    getSandboxServiceClient(): BaseSandboxService {
        return this.deploymentManager.getClient();
    }

    isCodeGenerating(): boolean {
        return this.generationPromise !== null;
    }

    getUserModelConfigs(): Record<AgentActionKey, ModelConfig> | undefined {
        return this.userModelConfigs;
    }

    setUserModelConfigs(configs: Record<AgentActionKey, ModelConfig> | undefined): void {
        this.userModelConfigs = configs;
    }

    getRuntimeOverrides(): InferenceRuntimeOverrides | undefined {
        return this.runtimeOverrides;
    }

    setRuntimeOverrides(overrides: InferenceRuntimeOverrides | undefined): void {
        this.runtimeOverrides = overrides;
    }

    abstract getOperationOptions(): OperationOptions;

    /**
     * Gets or creates an abort controller for the current operation
     * Reuses existing controller for nested operations (e.g., tool calling)
     */
    protected getOrCreateAbortController(): AbortController {
        // Don't reuse aborted controllers
        if (this.currentAbortController && !this.currentAbortController.signal.aborted) {
            return this.currentAbortController;
        }
        
        // Create new controller in memory for new operation
        this.currentAbortController = new AbortController();
        
        return this.currentAbortController;
    }
    
    /**
     * Cancels the current inference operation if any
     */
    public cancelCurrentInference(): boolean {
        if (this.currentAbortController) {
            this.logger.info('Cancelling current inference operation');
            this.currentAbortController.abort();
            this.currentAbortController = undefined;
            return true;
        }
        return false;
    }
    
    /**
     * Clears abort controller after successful completion
     */
    protected clearAbortController(): void {
        this.currentAbortController = undefined;
    }
    
    /**
     * Gets inference context with abort signal
     * Reuses existing abort controller for nested operations
     */
    protected getInferenceContext(): InferenceContext {
        const controller = this.getOrCreateAbortController();

        // The encrypted Cloudflare OAuth blob is kept fresh on `state.cloudflareToken`
        // (updated on WS connect and on transparent refresh). Pass it as a top-level
        // context field so `infer()` can decrypt the most recent access token.
        const liveBlob = this.state.cloudflareToken ?? null;

        return {
            metadata: this.state.metadata,
            enableFastSmartCodeFix: false,  // TODO: Do we want to enable it via some config?
            enableRealtimeCodeFix: false,   // TODO: Do we want to enable it via some config?
            abortSignal: controller.signal,
            userModelConfigs: this.getUserModelConfigs(),
            runtimeOverrides: this.getRuntimeOverrides(),
            userApiToken: liveBlob,
            onUsageConsumed: () => {
                this.broadcast(WebSocketMessageResponses.USAGE_UPDATED, {
                    message: 'Usage data updated',
                });
            },
        };
    }

    async generateReadme() {
        this.logger.info('Generating README.md');
        this.broadcast(WebSocketMessageResponses.FILE_GENERATING, {
            message: 'Generating README.md',
            filePath: 'README.md',
            filePurpose: 'Project documentation and setup instructions'
        });

        const readme = await this.operations.simpleGenerateFiles.generateReadme(this.getOperationOptions());

        await this.fileManager.saveGeneratedFile(readme, "feat: README.md");

        this.broadcast(WebSocketMessageResponses.FILE_GENERATED, {
            message: 'README.md generated successfully',
            file: readme
        });
        this.logger.info('README.md generated successfully');
    }

    async setBlueprint(blueprint: Blueprint): Promise<void> {
        this.setState({
            ...this.state,
            blueprint: blueprint as AgenticBlueprint | PhasicBlueprint,
        });
        this.broadcast(WebSocketMessageResponses.BLUEPRINT_UPDATED, {
            message: 'Blueprint updated',
            updatedKeys: Object.keys(blueprint || {}),
            blueprint: blueprint as AgenticBlueprint | PhasicBlueprint,
        });
    }

    getProjectType() {
        return this.state.projectType;
    }

    async queueUserRequest(request: string, images?: ProcessedImageAttachment[]): Promise<void> {
        this.setState({
            ...this.state,
            pendingUserInputs: [...this.state.pendingUserInputs, request]
        });
        if (images && images.length > 0) {
            this.logger.info('Storing user images in-memory for phase generation', {
                imageCount: images.length,
            });
            this.pendingUserImages = [...this.pendingUserImages, ...images];
        }
    }

    protected fetchPendingUserRequests(): string[] {
        const inputs = this.state.pendingUserInputs;
        if (inputs.length > 0) {
            this.setState({
                ...this.state,
                pendingUserInputs: []
            });
        }
        return inputs;
    }

    clearConversation(): void {
        this.infrastructure.clearConversation();
    }

    getGit(): GitVersionControl {
        return this.git;
    }


    /**
     * State machine controller for code generation with user interaction support
     * Executes phases sequentially with review cycles and proper state transitions
     */
    async generateAllFiles(): Promise<void> {
        if (this.state.mvpGenerated && this.state.pendingUserInputs.length === 0) {
            this.logger.info("Code generation already completed and no user inputs pending");
            return;
        }
        if (this.isCodeGenerating()) {
            this.logger.info("Code generation already in progress");
            return;
        }
        this.generationPromise = this.buildWrapper();
        await this.generationPromise;
    }

    setMVPGenerated(): boolean {
        if (!this.state.mvpGenerated) {
            this.setState({ ...this.state, mvpGenerated: true });
            this.logger.info('MVP generated');
            return true;
        }
        return false;
    }

    isMVPGenerated(): boolean {
        return this.state.mvpGenerated;
    }

    private async buildWrapper() {
        this.broadcast(WebSocketMessageResponses.GENERATION_STARTED, {
            message: 'Starting code generation',
            totalFiles: this.getTotalFiles()
        });
        this.logger.info('Starting code generation', {
            totalFiles: this.getTotalFiles()
        });
        await this.ensureTemplateDetails();
        try {
            await this.build();
        } catch (error) {
            if (error instanceof RateLimitExceededError) {
                this.logger.error("Error in state machine:", error);
                this.broadcast(WebSocketMessageResponses.RATE_LIMIT_ERROR, { error });
            } else {
                this.broadcastError("Error during generation", error);
            }
        } finally {
            // Clear abort controller after generation completes
            this.clearAbortController();
            
            const appService = new AppService(this.env);
            await appService.updateApp(
                this.getAgentId(),
                {
                    status: 'completed',
                }
            );
            this.generationPromise = null;
            this.broadcast(WebSocketMessageResponses.GENERATION_COMPLETE, {
                message: "Code generation and review process completed.",
                instanceId: this.state.sandboxInstanceId,
            });
        }
    }
    
    /**
     * Abstract method to be implemented by subclasses
     * Contains the main logic for code generation and review process
     */
    abstract build(): Promise<void>

    async executeDeepDebug(
        issue: string,
        toolRenderer: RenderToolCall,
        streamCb: (chunk: string) => void,
        focusPaths?: string[],
    ): Promise<DeepDebugResult> {
        const debugPromise = (async () => {
            try {
                const previousTranscript = this.state.lastDeepDebugTranscript ?? undefined;
                const operationOptions = this.getOperationOptions();
                const filesIndex = operationOptions.context.allFiles
                    .filter((f) =>
                        !focusPaths?.length ||
                        focusPaths.some((p) => f.filePath.includes(p)),
                    );

                const runtimeErrors = await this.fetchRuntimeErrors(false);

                const inputs: DeepDebuggerInputs = {
                    issue,
                    previousTranscript,
                    filesIndex,
                    runtimeErrors,
                    streamCb,
                    toolRenderer,
                };

                const operation = new DeepDebuggerOperation();

                const result = await operation.execute(inputs, operationOptions);

                const transcript = result.transcript;

                // Save transcript for next session
                this.setState({
                    ...this.state,
                    lastDeepDebugTranscript: transcript,
                });

                return { success: true as const, transcript };
            } catch (e) {
                this.logger.error('Deep debugger failed', e);
                return { success: false as const, error: `Deep debugger failed: ${String(e)}` };
            } finally {
                this.deepDebugPromise = null;
                this.deepDebugConversationId = null;
            }
        })();

        // Store promise before awaiting
        this.deepDebugPromise = debugPromise;

        return await debugPromise;
    }


    getModelConfigsInfo() {
        const modelService = new ModelConfigService(this.env);
        return modelService.getModelConfigsInfo(this.state.metadata.userId);
    }

    getTotalFiles(): number {
        return this.fileManager.getGeneratedFilePaths().length
    }

    getSummary(): Promise<AgentSummary> {
        const summaryData = {
            query: this.state.query,
            generatedCode: this.fileManager.getGeneratedFiles(),
        };
        return Promise.resolve(summaryData);
    }

    async getFullState(): Promise<TState> {
        return this.state;
    }
    
    migrateStateIfNeeded(): void {
        // no-op, only older phasic agents need this, for now.
    }

    getFileGenerated(filePath: string) {
        return this.fileManager!.getGeneratedFile(filePath) || null;
    }

    async fetchRuntimeErrors(clear: boolean = true, shouldWait: boolean = true): Promise<RuntimeError[]> {
        if (shouldWait) {
            await this.deploymentManager.waitForPreview();
        }

        try {
            const errors = await this.deploymentManager.fetchRuntimeErrors(clear);
            
            if (errors.length > 0) {
                this.broadcast(WebSocketMessageResponses.RUNTIME_ERROR_FOUND, {
                    errors,
                    message: "Runtime errors found",
                    count: errors.length
                });
            }

            return errors;
        } catch (error) {
            this.logger.error("Exception fetching runtime errors:", error);
            // If fetch fails, initiate redeploy
            this.deployToSandbox();
            const message = "<runtime errors not available at the moment as preview is not deployed>";
            return [{ message, timestamp: new Date().toISOString(), level: 0, rawOutput: message }];
        }
    }

    /**
     * Perform static code analysis on the generated files
     * This helps catch potential issues early in the development process
     */
    async runStaticAnalysisCode(files?: string[]): Promise<StaticAnalysisResponse> {
        try {
            // Only use cache for full (unscoped) analysis
            if (!files && this.staticAnalysisCache) {
                return this.staticAnalysisCache;
            }

            // Use in-memory analysis for browser-rendered projects (no sandbox)
            const templateDetails = this.getTemplateDetails();
            let analysisResponse: StaticAnalysisResponse;

            if (templateDetails?.renderMode === 'browser') {
                analysisResponse = await this.runInMemoryAnalysis(files);
            } else {
                analysisResponse = await this.deploymentManager.runStaticAnalysis(files);
            }

            // Only cache full (unscoped) analysis results
            if (!files) {
                this.staticAnalysisCache = analysisResponse;
            }

            const { lint, typecheck } = analysisResponse;
            this.broadcast(WebSocketMessageResponses.STATIC_ANALYSIS_RESULTS, {
                lint: { issues: lint.issues, summary: lint.summary },
                typecheck: { issues: typecheck.issues, summary: typecheck.summary }
            });

            return analysisResponse;
        } catch (error) {
            this.broadcastError("Failed to lint code", error);
            return { success: false, lint: { issues: [], }, typecheck: { issues: [], } };
        }
    }

    /**
     * Run in-memory static analysis for browser-rendered projects
     * Performs static analysis directly in the worker without using the sandbox
     */
    private async runInMemoryAnalysis(filePaths?: string[]): Promise<StaticAnalysisResponse> {
        const allFiles = this.fileManager.getAllFiles();
        const filePathSet = filePaths ? new Set(filePaths) : null;
        const filesToAnalyze = filePathSet
            ? allFiles.filter((f) => filePathSet.has(f.filePath))
            : allFiles;

        const fileInputs = filesToAnalyze.map((f) => ({
            path: f.filePath,
            content: f.fileContents,
        }));

        const analyzer = new InMemoryAnalyzer();
        return analyzer.analyze(fileInputs);
    }

    /**
     * Apply deterministic code fixes for common TypeScript errors
     */
    protected async applyDeterministicCodeFixes() : Promise<StaticAnalysisResponse | undefined> {
        try {
            // Get static analysis and do deterministic fixes
            const staticAnalysis = await this.runStaticAnalysisCode();
            if (staticAnalysis.typecheck.issues.length == 0) {
                this.logger.info("No typecheck issues found, skipping deterministic fixes");
                return staticAnalysis;  // So that static analysis is not repeated again
            }
            const typeCheckIssues = staticAnalysis.typecheck.issues;
            this.broadcast(WebSocketMessageResponses.DETERMINISTIC_CODE_FIX_STARTED, {
                message: `Attempting to fix ${typeCheckIssues.length} TypeScript issues using deterministic code fixer`,
                issues: typeCheckIssues
            });

            this.logger.info(`Attempting to fix ${typeCheckIssues.length} TypeScript issues using deterministic code fixer`);
            const allFiles = this.fileManager.getAllFiles();

            const fixResult = fixProjectIssues(
                allFiles.map(file => ({
                    filePath: file.filePath,
                    fileContents: file.fileContents,
                    filePurpose: ''
                })),
                typeCheckIssues
            );

            this.broadcast(WebSocketMessageResponses.DETERMINISTIC_CODE_FIX_COMPLETED, {
                message: `Fixed ${typeCheckIssues.length} TypeScript issues using deterministic code fixer`,
                issues: typeCheckIssues,
                fixResult
            });

            if (fixResult) {
                // If there are unfixable issues but of type TS2307, extract external module names and install them
                if (fixResult.unfixableIssues.length > 0) {
                    const modulesNotFound = fixResult.unfixableIssues.filter(issue => issue.issueCode === 'TS2307');
                    // Reason is of the form: External package "xyz" should be handled by package manager                    
                    const moduleNames = modulesNotFound.flatMap(issue => {
                        const match = issue.reason.match(/External package ["'](.+?)["']/);
                        const name = match?.[1];
                        return (typeof name === 'string' && name.trim().length > 0 && !name.startsWith('@shared')) ? [name] : [];
                    }).filter((name) => !name.includes('cloudflare:'));
                    if (moduleNames.length > 0) {
                        const installCommands = moduleNames.map(moduleName => `bun install ${moduleName}`);
                        await this.executeCommands(installCommands, false);

                        this.logger.info(`Deterministic code fixer installed missing modules: ${moduleNames.join(', ')}`);
                    } else {
                        this.logger.info(`Deterministic code fixer detected no external modules to install from unfixable TS2307 issues`);
                    }
                }
                if (fixResult.modifiedFiles.length > 0) {
                        this.logger.info("Applying deterministic fixes to files, Fixes: ", JSON.stringify(fixResult, null, 2));
                        const fixedFiles = fixResult.modifiedFiles.map(file => ({
                            filePath: file.filePath,
                            filePurpose: allFiles.find(f => f.filePath === file.filePath)?.filePurpose || '',
                            fileContents: file.fileContents
                    }));
                    await this.fileManager.saveGeneratedFiles(fixedFiles, "fix: applied deterministic fixes");
                    
                    await this.deployToSandbox(fixedFiles, false, "fix: applied deterministic fixes");
                    this.logger.info("Deployed deterministic fixes to sandbox");
                }
            }
            this.logger.info(`Applied deterministic code fixes: ${JSON.stringify(fixResult, null, 2)}`);
        } catch (error) {
            this.broadcastError('Deterministic code fixer failed', error);
        }
        // return undefined;
    }

    async fetchAllIssues(resetIssues: boolean = false): Promise<AllIssues> {
        const templateDetails = this.getTemplateDetails();
        const isBrowserOnly = templateDetails?.renderMode === 'browser';

        // For browser-rendered projects (no sandbox), only run static analysis
        if (isBrowserOnly) {
            const staticAnalysis = await this.runStaticAnalysisCode();
            this.logger.info("Fetched issues (browser-rendered):", JSON.stringify({ runtimeErrors: [], staticAnalysis }));
            return { runtimeErrors: [], staticAnalysis };
        }
        if (!await this.waitForSandboxReady()) {
            return { runtimeErrors: [], staticAnalysis: { success: false, lint: { issues: [] }, typecheck: { issues: [] } } };
        }
        const [runtimeErrors, staticAnalysis] = await Promise.all([
            this.fetchRuntimeErrors(resetIssues),
            this.runStaticAnalysisCode()
        ]);
        this.logger.info("Fetched all issues:", JSON.stringify({ runtimeErrors, staticAnalysis }));
        
        return { runtimeErrors, staticAnalysis };
    }

    async updateProjectName(newName: string): Promise<boolean> {
        try {
            const valid = /^[a-z0-9-_]{3,50}$/.test(newName);
            if (!valid) return false;
            const updatedBlueprint = { ...this.state.blueprint, projectName: newName };
            this.setState({
                ...this.state,
                blueprint: updatedBlueprint
            });
            let ok = true;
            if (this.state.sandboxInstanceId) {
                try {
                    ok = await this.getSandboxServiceClient().updateProjectName(this.state.sandboxInstanceId, newName);
                } catch (_) {
                    ok = false;
                }
            }
            try {
                const appService = new AppService(this.env);
                const dbOk = await appService.updateApp(this.getAgentId(), { title: newName });
                ok = ok && dbOk;
            } catch (error) {
                this.logger.error('Error updating project name in database:', error);
                ok = false;
            }
            this.broadcast(WebSocketMessageResponses.PROJECT_NAME_UPDATED, {
                message: 'Project name updated',
                projectName: newName
            });
            return ok;
        } catch (error) {
            this.logger.error('Error updating project name:', error);
            return false;
        }
    }

    /**
     * Update user-facing blueprint fields
     * Only allows updating safe, cosmetic fields - not internal generation state
     */
    async updateBlueprint(patch: Partial<Blueprint>): Promise<Blueprint> {
        // Fields that are safe to update after generation starts
        // Excludes: initialPhase (breaks phasic generation)
        const safeUpdatableFields = new Set([
            'title',
            'description',
            'detailedDescription',
            'colorPalette',
            'views',
            'userFlow',
            'dataFlow',
            'architecture',
            'pitfalls',
            'frameworks',
            'implementationRoadmap'
        ]);

        // Filter to only safe fields
        const filtered: Record<string, unknown> = {};
        for (const [key, value] of Object.entries(patch)) {
            if (safeUpdatableFields.has(key) && value !== undefined) {
                filtered[key] = value;
            }
        }

        // Agentic: allow initializing plan if not set yet (first-time plan initialization only)
        if (this.isAgenticState(this.state)) {
            const currentPlan = this.state.blueprint?.plan;
            const patchPlan = 'plan' in patch ? patch.plan : undefined;
            if (Array.isArray(patchPlan) && (!Array.isArray(currentPlan) || currentPlan.length === 0)) {
                filtered['plan'] = patchPlan;
            }
        }

        // projectName requires sandbox update, handle separately
        if ('projectName' in patch && typeof patch.projectName === 'string') {
            await this.updateProjectName(patch.projectName);
        }

        // Merge and update state
        const updated = { ...this.state.blueprint, ...filtered } as Blueprint;
        this.setState({
            ...this.state,
            blueprint: updated
        });
        
        this.broadcast(WebSocketMessageResponses.BLUEPRINT_UPDATED, {
            message: 'Blueprint updated',
            updatedKeys: Object.keys(filtered),
            blueprint: updated as AgenticBlueprint | PhasicBlueprint,
        });
        
        return updated;
    }

    // ===== Debugging helpers for assistants =====
    listFiles(): FileOutputType[] {
        return this.fileManager.getAllRelevantFiles();
    }

    async readFiles(paths: string[]): Promise<{ files: { path: string; content: string }[] }> {
        const results: { path: string; content: string }[] = [];
        const notFoundInFileManager: string[] = [];

        // First, try to read from FileManager (template + generated files)
        for (const path of paths) {
            const file = this.fileManager.getFile(path);
            if (file) {
                results.push({ path, content: file.fileContents });
            } else {
                notFoundInFileManager.push(path);
            }
        }

        // If some files not found in FileManager and sandbox exists, try sandbox
        if (notFoundInFileManager.length > 0 && this.state.sandboxInstanceId) {
            const resp = await this.getSandboxServiceClient().getFiles(
                this.state.sandboxInstanceId,
                notFoundInFileManager
            );
            if (resp.success) {
                results.push(...resp.files.map(f => ({
                    path: f.filePath,
                    content: f.fileContents
                })));
            }
        }

        return { files: results };
    }

    async execCommands(commands: string[], shouldSave: boolean, timeout?: number): Promise<ExecuteCommandsResponse> {
        const { sandboxInstanceId } = this.state;
        if (!sandboxInstanceId) {
            return { success: false, results: [], error: 'No sandbox instance' };
        }
        const result = await this.getSandboxServiceClient().executeCommands(sandboxInstanceId, commands, timeout);
        if (shouldSave) {
            // Only persist commands that actually succeeded in the sandbox. Saving the requested
            // commands unconditionally would let a command that failed here still be embedded in
            // the bootstrap history and re-run on a victim's machine (VEC-C).
            const successfulCommands = result.results.filter(r => r.success).map(r => r.command);
            this.saveExecutedCommands(successfulCommands);
        }
        return result;
    }

    updateSlideManifest(file: FileOutputType) {
        // If the project type is presentation and this is a slide file, update the manifest
        if (this.projectType === 'presentation') {
            const templateDetails = this.getTemplateDetails()
            if (!templateDetails) {
                return;
            }
            const slidesDirectory = templateDetails.slideDirectory ?? '/public/slides';
            if (file.filePath.startsWith(slidesDirectory) && file.filePath.endsWith('.json')) {
                const manifestPath = `${slidesDirectory}/manifest.json`
                const existingManifest = this.fileManager.getFile(manifestPath)
                
                // Parse existing manifest or create new one
                let manifestData: { slides: string[] } = { slides: [] };
                if (existingManifest) {
                    try {
                        const parsed = JSON.parse(existingManifest.fileContents);
                        manifestData = {
                            slides: Array.isArray(parsed.slides) ? parsed.slides : []
                        };
                    } catch (error) {
                        this.logger.error('Failed to parse existing manifest.json', error);
                        manifestData = { slides: [] };
                    }
                } else {
                    manifestData = { slides: [] };
                }
                
                // Add slide path to slides array if not already present
                const relativeSlidePath = file.filePath.replace(slidesDirectory + '/', '');
                if (!manifestData.slides.includes(relativeSlidePath)) {
                    manifestData.slides.push(relativeSlidePath);
                    
                    // Save updated manifest
                    const updatedManifest: FileOutputType = {
                        filePath: manifestPath,
                        fileContents: JSON.stringify(manifestData, null, 2),
                        filePurpose: 'Presentation slides manifest'
                    };
                    this.fileManager.recordFileChanges([updatedManifest]);
                    
                    this.logger.info('Updated manifest.json with new slide', {
                        slidePath: relativeSlidePath,
                        totalSlides: manifestData.slides.length
                    });
                }
            }
        }
    }

    /**
     * Regenerate a file to fix identified issues
     * Retries up to 3 times before giving up
     */
    async regenerateFile(file: FileOutputType, issues: string[], retryIndex: number = 0) {
        this.broadcast(WebSocketMessageResponses.FILE_REGENERATING, {
            message: `Regenerating file: ${file.filePath}`,
            filePath: file.filePath,
            original_issues: issues,
        });
        
        const result = await this.operations.regenerateFile.execute(
            {file, issues, retryIndex},
            this.getOperationOptions()
        );

        this.updateSlideManifest(result);
        const fileState = await this.fileManager.saveGeneratedFile(result);

        this.broadcast(WebSocketMessageResponses.FILE_REGENERATED, {
            message: `Regenerated file: ${file.filePath}`,
            file: fileState,
            original_issues: issues,
        });
        
        return fileState;
    }

    async regenerateFileByPath(path: string, issues: string[]): Promise<{ path: string; diff: string }> {
        const templateDetails = this.getTemplateDetails();
        if (templateDetails && templateDetails.dontTouchFiles && templateDetails.dontTouchFiles.includes(path)) {
            return {
                path,
                diff: '<WRITE PROTECTED - TEMPLATE FILE, CANNOT MODIFY - SKIPPED - NO CHANGES MADE>'
            };
        }
        // Prefer local file manager; fallback to sandbox
        let fileContents = '';
        let filePurpose = '';
        try {
            const fmFile = this.fileManager.getFile(path);
            if (fmFile) {
                fileContents = fmFile.fileContents;
                filePurpose = fmFile.filePurpose || '';
            } else {
                const { sandboxInstanceId } = this.state;
                if (!sandboxInstanceId) {
                    throw new Error('No sandbox instance available');
                }
                const resp = await this.getSandboxServiceClient().getFiles(sandboxInstanceId, [path]);
                const f = resp.success ? resp.files.find(f => f.filePath === path) : undefined;
                if (!f) throw new Error(resp.error || `File not found: ${path}`);
                fileContents = f.fileContents;
            }
        } catch (e) {
            throw new Error(`Failed to read file for regeneration: ${String(e)}`);
        }

        const regenerated = await this.regenerateFile({ filePath: path, fileContents, filePurpose }, issues, 0);
        // Invalidate cache
        this.staticAnalysisCache = null;
        // Persist to sandbox instance
        // await this.getSandboxServiceClient().writeFiles(sandboxInstanceId, [{ filePath: regenerated.filePath, fileContents: regenerated.fileContents }], `Deep debugger fix: ${path}`);
        await this.deploymentManager.deployToSandbox([regenerated])
        return { path, diff: regenerated.lastDiff };
    }

    async generateFiles(
        phaseName: string,
        phaseDescription: string,
        requirements: string[],
        files: FileConceptType[]
    ): Promise<{ files: Array<{ path: string; purpose: string; diff: string }> }> {
        this.logger.info('Generating files for deep debugger', {
            phaseName,
            requirementsCount: requirements.length,
            filesCount: files.length
        });
        
        // Broadcast file generation started
        this.broadcast(WebSocketMessageResponses.PHASE_IMPLEMENTING, {
            message: `Generating files: ${phaseName}`,
            phaseName
        });

        const skippedFiles: { path: string; purpose: string; diff: string }[] = [];

        // Enforce template donttouch constraints
        const templateDetails = this.getTemplateDetails();
        if (templateDetails && templateDetails.dontTouchFiles) {
            const dontTouchFiles = new Set<string>(templateDetails.dontTouchFiles);
            files = files.filter(file => {
                if (dontTouchFiles.has(file.path)) {
                    this.logger.info('Skipping dont-touch file', { filePath: file.path });
                    skippedFiles.push({ path: file.path, purpose: `WRITE-PROTECTED FILE, CANNOT MODIFY`, diff: "<WRITE PROTECTED - TEMPLATE FILE, CANNOT MODIFY - SKIPPED - NO CHANGES MADE>" });
                    return false;
                }
                return true;
            });
        }

        const savedFiles: FileState[] = [];

        const operation = new SimpleCodeGenerationOperation();
        const result = await operation.execute(
            {
                phaseName,
                phaseDescription,
                requirements,
                files,
                fileGeneratingCallback: (filePath: string, filePurpose: string) => {
                    this.broadcast(WebSocketMessageResponses.FILE_GENERATING, {
                        message: `Generating file: ${filePath}`,
                        filePath,
                        filePurpose
                    });
                },
                fileChunkGeneratedCallback: (filePath: string, chunk: string, format: 'full_content' | 'unified_diff') => {
                    this.broadcast(WebSocketMessageResponses.FILE_CHUNK_GENERATED, {
                        message: `Generating file: ${filePath}`,
                        filePath,
                        chunk,
                        format
                    });
                },
                fileClosedCallback: (file, message) => {
                    // Record file to state (sync)
                    const saved = this.fileManager.recordFileChanges([file]);
                    savedFiles.push(...saved);
                    this.updateSlideManifest(file);
                    this.broadcast(WebSocketMessageResponses.FILE_GENERATED, {
                        message,
                        file
                    });
                }
            },
            this.getOperationOptions()
        );

        await this.fileManager.saveGeneratedFiles(
            [],
            `feat: ${phaseName}\n\n${phaseDescription}`
        );

        this.logger.info('Files generated and saved', {
            fileCount: result.files.length
        });

        await this.deployToSandbox(savedFiles, false);

        return { 
            files: [
                ...skippedFiles,
                ...savedFiles.map(f => {
                    return {
                        path: f.filePath,
                        purpose: f.filePurpose || '',
                        diff: f.lastDiff || ''
                    };
                }) 
            ]
        };
    }

    /**
     * Get or create file serving token (lazy generation)
     */
    private getOrCreateFileServingToken(): string {
        if (!this.state.fileServingToken) {
            const token = generatePortToken();
            this.setState({
                ...this.state,
                fileServingToken: {
                    token,
                    createdAt: Date.now()
                }
            });
        }
        return this.state.fileServingToken!.token;
    }

    /**
     * Get browser preview URL for file serving.
     *
     * Host resolution order: frontend-supplied origin (captured at WS
     * upgrade) → configured preview domain → CUSTOM_DOMAIN → localhost
     * fallback. In dev when the FE is on localhost we keep the dev
     * server host (`localhost:5173`) so the iframe URL matches.
     */
    public async getBrowserPreviewURL(): Promise<string> {
        const token = this.getOrCreateFileServingToken();
        const agentId = this.getAgentId();
        const previewDomain = isDev(this.env)
            ? 'localhost:5173'
            : resolvePreviewHost(this.env, this.state.wsOrigin);

        // Format: b-{agentid}-{token}.{previewDomain}
        return `${getProtocolForHost(previewDomain)}://b-${agentId}-${token}.${previewDomain}`;
    }

    // A wrapper for LLM tool to deploy to sandbox
    async deployPreview(clearLogs: boolean = true, forceRedeploy: boolean = false): Promise<string> {
        const response = await this.deployToSandbox([], forceRedeploy, undefined, clearLogs);
        if (response && response.previewURL) {
            this.broadcast(WebSocketMessageResponses.PREVIEW_FORCE_REFRESH, {});
            return `Deployment successful: ${response.previewURL}`;
        }
        return `Failed to deploy: ${response?.tunnelURL}`;
    }

    async deployToSandbox(files: FileOutputType[] = [], redeploy: boolean = false, commitMessage?: string, clearLogs: boolean = false): Promise<PreviewType | null> {
        // Only deploy if project is previewable
        if (!this.isPreviewable()) {
            throw new Error('Project is not previewable');
        }
        this.logger.info('[AGENT] Deploying to sandbox', { files: files.length, redeploy, commitMessage, renderMode: this.getTemplateDetails()?.renderMode, templateDetails: this.getTemplateDetails() });

        if (this.getTemplateDetails()?.renderMode === 'browser') {
            this.logger.info('Deploying to browser native sandbox');
            this.broadcast(WebSocketMessageResponses.DEPLOYMENT_STARTED, {});
            const result: PreviewType = {
                previewURL: await this.getBrowserPreviewURL()
            }
            this.logger.info('Deployed to browser native sandbox');
            this.broadcast(WebSocketMessageResponses.DEPLOYMENT_COMPLETED, result);
            return result;
        }
            
        // Invalidate static analysis cache
        this.staticAnalysisCache = null;
        
        // Call deployment manager with callbacks for broadcasting at the right times
        const result = await this.deploymentManager.deployToSandbox(
            files,
            redeploy,
            commitMessage,
            clearLogs,
            {
                onStarted: (data) => {
                    this.broadcast(WebSocketMessageResponses.DEPLOYMENT_STARTED, data);
                },
                onCompleted: (data) => {
                    this.broadcast(WebSocketMessageResponses.DEPLOYMENT_COMPLETED, data);
                },
                onError: (data) => {
                    this.broadcast(WebSocketMessageResponses.DEPLOYMENT_FAILED, data);
                },
                onAfterSetupCommands: async () => {
                    // Sync package.json after setup commands (includes dependency installs)
                    await this.syncPackageJsonFromSandbox();
                }
            }
        );

        this.resolveSandboxReady();
        return result;
    }
    
    /**
     * Deploy the generated code to Cloudflare Workers
     */
    async deployToCloudflare(target: DeploymentTarget = 'platform'): Promise<{ deploymentUrl?: string; workersUrl?: string } | null> {
        try {
            // Ensure sandbox instance exists first
            if (!this.state.sandboxInstanceId) {
                this.logger.info('No sandbox instance, deploying to sandbox first');
                await this.deployToSandbox();
                
                if (!this.state.sandboxInstanceId) {
                    this.logger.error('Failed to deploy to sandbox service');
                    this.broadcast(WebSocketMessageResponses.CLOUDFLARE_DEPLOYMENT_ERROR, {
                        message: 'Deployment failed: Failed to deploy to sandbox service',
                        error: 'Sandbox service unavailable'
                    });
                    return null;
                }
            }

            // Call service - handles orchestration, callbacks for broadcasting
            const result = await this.deploymentManager.deployToCloudflare({
                target,
                callbacks: {
                    onStarted: (data) => {
                        this.broadcast(WebSocketMessageResponses.CLOUDFLARE_DEPLOYMENT_STARTED, data);
                    },
                    onCompleted: (data) => {
                        this.broadcast(WebSocketMessageResponses.CLOUDFLARE_DEPLOYMENT_COMPLETED, data);
                    },
                    onError: (data) => {
                        this.broadcast(WebSocketMessageResponses.CLOUDFLARE_DEPLOYMENT_ERROR, data);
                    },
                }
            });

            // Update database with deployment ID if successful
            if (result.deploymentUrl && result.deploymentId) {
                const appService = new AppService(this.env);
                await appService.updateDeploymentId(
                    this.getAgentId(),
                    result.deploymentId
                );
            }

            return result.deploymentUrl ? { deploymentUrl: result.deploymentUrl } : null;

        } catch (error) {
            this.logger.error('Cloudflare deployment error:', error);
            this.broadcast(WebSocketMessageResponses.CLOUDFLARE_DEPLOYMENT_ERROR, {
                message: 'Deployment failed',
                error: error instanceof Error ? error.message : String(error)
            });
            return null;
        }
    }

    async importTemplate(templateName: string): Promise<{ templateName: string; filesImported: number; files: TemplateFile[] }> {
        this.logger.info(`Importing template into project: ${templateName}`);

        if (this.state.templateName !== templateName) {
            // Get template catalog info to sync projectType
            const catalogResponse = await BaseSandboxService.listTemplates();
            const catalogInfo = catalogResponse.success 
                ? catalogResponse.templates.find(t => t.name === templateName)
                : null;
            
            // Update state with template name and projectType if available
            this.setState({
                ...this.state,
                templateName: templateName,
                ...(catalogInfo?.projectType ? { projectType: catalogInfo.projectType } : {}),
            });

            this.templateDetailsCache = null;   // Clear template details cache
        }
        const templateDetails = await this.ensureTemplateDetails();
        if (!templateDetails) {
            throw new Error(`Failed to get template details for: ${templateName}`);
        }

        this.setState({
            ...this.state,
            lastPackageJson: templateDetails.allFiles['package.json'] || this.state.lastPackageJson,
        });

        // Get important files for return value
        const importantFiles = getTemplateImportantFiles(templateDetails);

        // Ensure deployment to sandbox 
        await this.deployToSandbox();

        // Notify frontend about template metadata update
        this.broadcast(WebSocketMessageResponses.TEMPLATE_UPDATED, {
            templateDetails
        });

        return {
            templateName: templateDetails.name,
            filesImported: Object.keys(templateDetails.allFiles).length,
            files: importantFiles
        };
    }

    async waitForGeneration(): Promise<void> {
        if (this.generationPromise) {
            try {
                await this.generationPromise;
                this.logger.info("Code generation completed successfully");
            } catch (error) {
                this.logger.error("Error during code generation:", error);
            }
        } else {
            this.logger.error("No generation process found");
        }
    }

    isDeepDebugging(): boolean {
        return this.deepDebugPromise !== null;
    }
    
    getDeepDebugSessionState(): { conversationId: string } | null {
        if (this.deepDebugConversationId && this.deepDebugPromise) {
            return { conversationId: this.deepDebugConversationId };
        }
        return null;
    }

    async waitForDeepDebug(): Promise<void> {
        if (this.deepDebugPromise) {
            try {
                await this.deepDebugPromise;
                this.logger.info("Deep debug session completed successfully");
            } catch (error) {
                this.logger.error("Error during deep debug session:", error);
            } finally {
                // Clear promise after waiting completes
                this.deepDebugPromise = null;
            }
        }
    }

    protected async onProjectUpdate(message: string): Promise<void> {
        this.setState({
            ...this.state,
            projectUpdatesAccumulator: [...this.state.projectUpdatesAccumulator, message]
        });
    }

    protected async getAndResetProjectUpdates() {
        const projectUpdates = this.state.projectUpdatesAccumulator || [];
        this.setState({
            ...this.state,
            projectUpdatesAccumulator: []
        });
        return projectUpdates;
    }

    public broadcast<T extends WebSocketMessageType>(msg: T, data?: WebSocketMessageData<T>): void {
        if (this.operations.processUserMessage.isProjectUpdateType(msg)) {
            let message = msg as string;
            if (data && 'message' in data) {
                message = (data as { message: string }).message;
            }
            this.onProjectUpdate(message);
        }
        super.broadcast(msg, data);
    }

    protected getBootstrapCommands() {
        const bootstrapCommands = this.state.commandsHistory || [];
        // Validate, deduplicate, and clean
        const { validCommands } = validateAndCleanBootstrapCommands(bootstrapCommands);
        return validCommands;
    }

    protected async saveExecutedCommands(commands: string[]) {
        this.logger.info('Saving executed commands', { commands });
        
        // Merge with existing history
        const mergedCommands = [...(this.state.commandsHistory || []), ...commands];
        
        // Validate, deduplicate, and clean
        const { validCommands, invalidCommands, deduplicated } = validateAndCleanBootstrapCommands(mergedCommands);

        // Log what was filtered out
        if (invalidCommands.length > 0 || deduplicated > 0) {
            this.logger.warn('[commands] Bootstrap commands cleaned', { 
                invalidCommands,
                invalidCount: invalidCommands.length,
                deduplicatedCount: deduplicated,
                finalCount: validCommands.length
            });
        }

        // Update state with cleaned commands
        this.setState({
            ...this.state,
            commandsHistory: validCommands
        });

        // Update bootstrap script with validated commands
        await this.updateBootstrapScript(validCommands);

        // Sync package.json if any dependency-modifying commands were executed
        const hasDependencyCommands = commands.some(cmd => 
            cmd.includes('install') || 
            cmd.includes(' add ') || 
            cmd.includes('remove') ||
            cmd.includes('uninstall')
        );
        
        if (hasDependencyCommands) {
            this.logger.info('Dependency commands executed, syncing package.json from sandbox');
            await this.syncPackageJsonFromSandbox();
        }
    }

    /**
     * Execute commands with retry logic
     * Chunks commands and retries failed ones with AI assistance
     */
    protected async executeCommands(commands: string[], shouldRetry: boolean = true, chunkSize: number = 5): Promise<void> {
        const state = this.state;
        if (!state.sandboxInstanceId) {
            this.logger.warn('No sandbox instance available for executing commands');
            return;
        }

        // Sanitize and prepare commands
        commands = commands.join('\n').split('\n').filter(cmd => cmd.trim() !== '').filter(cmd => looksLikeCommand(cmd) && !cmd.includes(' undefined'));
        if (commands.length === 0) {
            this.logger.warn("No commands to execute");
            return;
        }

        commands = commands.map(cmd => cmd.trim().replace(/^\s*-\s*/, '').replace(/^npm/, 'bun'));
        this.logger.info(`AI suggested ${commands.length} commands to run: ${commands.join(", ")}`);

        // Remove duplicate commands
        commands = Array.from(new Set(commands));

        // Execute in chunks
        const commandChunks = [];
        for (let i = 0; i < commands.length; i += chunkSize) {
            commandChunks.push(commands.slice(i, i + chunkSize));
        }

        const successfulCommands: string[] = [];

        for (const chunk of commandChunks) {
            // Retry failed commands up to 3 times
            let currentChunk = chunk;
            let retryCount = 0;
            const maxRetries = shouldRetry ? 3 : 1;
            
            while (currentChunk.length > 0 && retryCount < maxRetries) {
                try {
                    this.broadcast(WebSocketMessageResponses.COMMAND_EXECUTING, {
                        message: retryCount > 0 ? `Retrying commands (attempt ${retryCount + 1}/${maxRetries})` : "Executing commands",
                        commands: currentChunk
                    });
                    
                    const resp = await this.getSandboxServiceClient().executeCommands(
                        state.sandboxInstanceId,
                        currentChunk
                    );
                    if (!resp.results || !resp.success) {
                        this.logger.error('Failed to execute commands', { response: resp });
                        // Check if instance is still running
                        const status = await this.getSandboxServiceClient().getInstanceStatus(state.sandboxInstanceId);
                        if (!status.success || !status.isHealthy) {
                            this.logger.error(`Instance ${state.sandboxInstanceId} is no longer running`);
                            return;
                        }
                        break;
                    }

                    // Process results
                    const successful = resp.results.filter(r => r.success);
                    const failures = resp.results.filter(r => !r.success);

                    // Track successful commands
                    if (successful.length > 0) {
                        const successfulCmds = successful.map(r => r.command);
                        this.logger.info(`Successfully executed ${successful.length} commands: ${successfulCmds.join(", ")}`);
                        successfulCommands.push(...successfulCmds);
                    }

                    // If all succeeded, move to next chunk
                    if (failures.length === 0) {
                        this.logger.info(`All commands in chunk executed successfully`);
                        break;
                    }
                    
                    // Handle failures
                    const failedCommands = failures.map(r => r.command);
                    this.logger.warn(`${failures.length} commands failed: ${failedCommands.join(", ")}`);
                    
                    // Only retry if shouldRetry is true
                    if (!shouldRetry) {
                        break;
                    }
                    
                    retryCount++;
                    
                    // For install commands, try AI regeneration
                    const failedInstallCommands = failedCommands.filter(cmd => 
                        cmd.startsWith("bun") || cmd.startsWith("npm") || cmd.includes("install")
                    );
                    
                    if (failedInstallCommands.length > 0 && retryCount < maxRetries) {
                        // Use AI to suggest alternative commands
                        const newCommands = await this.getProjectSetupAssistant().generateSetupCommands(
                            `The following install commands failed: ${JSON.stringify(failures, null, 2)}. Please suggest alternative commands.`
                        );
                        
                        if (newCommands?.commands && newCommands.commands.length > 0) {
                            this.logger.info(`AI suggested ${newCommands.commands.length} alternative commands`);
                            this.broadcast(WebSocketMessageResponses.COMMAND_EXECUTING, {
                                message: "Executing regenerated commands",
                                commands: newCommands.commands
                            });
                            currentChunk = newCommands.commands.filter(looksLikeCommand);
                        } else {
                            this.logger.warn('AI could not generate alternative commands');
                            currentChunk = [];
                        }
                    } else {
                        // No retry needed for non-install commands
                        currentChunk = [];
                    }
                } catch (error) {
                    this.logger.error('Error executing commands:', error);
                    // Stop retrying on error
                    break;
                }
            }
        }

        // Record command execution history
        const failedCommands = commands.filter(cmd => !successfulCommands.includes(cmd));
        
        if (failedCommands.length > 0) {
            this.broadcastError('Failed to execute commands', new Error(failedCommands.join(", ")));
        } else {
            this.logger.info(`All commands executed successfully: ${successfulCommands.join(", ")}`);
        }

        this.saveExecutedCommands(successfulCommands);
    }

    /**
     * Sync package.json from sandbox to agent's git repository
     * Called after install/add/remove commands to keep dependencies in sync
     */
    protected async syncPackageJsonFromSandbox(): Promise<void> {
        try {
            this.logger.info('Fetching current package.json from sandbox');
            const results = await this.readFiles(['package.json']);
            if (!results || !results.files || results.files.length === 0) {
                this.logger.warn('Failed to fetch package.json from sandbox', { results });
                return;
            }
            const packageJsonContent = results.files[0].content;

            const { updated, packageJson } = updatePackageJson(this.state.lastPackageJson, packageJsonContent);
            if (!updated) {
                this.logger.info('package.json has not changed, skipping sync');
                return;
            }
            // Update state with latest package.json
            this.setState({
                ...this.state,
                lastPackageJson: packageJson
            });
            
            // Commit to git repository
            const fileState = await this.fileManager.saveGeneratedFile(
                {
                    filePath: 'package.json',
                    fileContents: packageJson,
                    filePurpose: 'Project dependencies and configuration'
                },
                'chore: sync package.json dependencies from sandbox',
                true
            );
            
            this.logger.info('Successfully synced package.json to git', { 
                filePath: fileState.filePath,
            });
            
            // Broadcast update to clients
            this.broadcast(WebSocketMessageResponses.FILE_GENERATED, {
                message: 'Synced package.json from sandbox',
                file: fileState
            });
            
        } catch (error) {
            this.logger.error('Failed to sync package.json from sandbox', error);
            // Non-critical error - don't throw, just log
        }
    }

    async getLogs(_reset?: boolean, durationSeconds?: number): Promise<string> {
        if (!this.state.sandboxInstanceId) {
            throw new Error('Cannot get logs: No sandbox instance available');
        }
        
        const response = await this.getSandboxServiceClient().getLogs(this.state.sandboxInstanceId, _reset, durationSeconds);
        if (response.success) {
            return `STDOUT: ${response.logs.stdout}\nSTDERR: ${response.logs.stderr}`;
        } else {
            return `Failed to get logs, ${response.error}`;
        }
    }

    /**
     * Delete files from the file manager
     */
    async deleteFiles(filePaths: string[]) : Promise<{ success: boolean, error?: string }> {
        const deleteCommands: string[] = [];
        for (const filePath of filePaths) {
            deleteCommands.push(`rm -rf ${filePath}`);
        }
        // Remove the files from file manager
        this.fileManager.deleteFiles(filePaths);
        try {
            await this.executeCommands(deleteCommands, false);
            this.logger.info(`Deleted ${filePaths.length} files: ${filePaths.join(", ")}`);
            return { success: true };
        } catch (error) {
            this.logger.error('Error deleting files:', error);
            return { success: false, error: error as string };
        }
    }

    /**
     * Handle user input during conversational code generation
     * Processes user messages and updates pendingUserInputs state
     */
    async handleUserInput(userMessage: string, images?: ImageAttachment[]): Promise<void> {
        try {
            this.logger.info('Processing user input message', { 
                messageLength: userMessage.length,
                pendingInputsCount: this.state.pendingUserInputs.length,
                hasImages: !!images && images.length > 0,
                imageCount: images?.length || 0
            });

            // Ensure template details are loaded before processing
            await this.ensureTemplateDetails();

            // Just fetch runtime errors
            const errors = await this.fetchRuntimeErrors(false, false);
            const projectUpdates = await this.getAndResetProjectUpdates();
            this.logger.info('Passing context to user conversation processor', { errors, projectUpdates });


            const conversationState = this.infrastructure.getConversationState();
            // If there are images, upload them and pass the URLs to the conversation processor
            let uploadedImages: ProcessedImageAttachment[] = [];
            if (images) {
                uploadedImages = await Promise.all(images.map(async (image) => {
                    return await uploadImage(this.env, image, ImageType.UPLOADS);
                }));

                this.logger.info('Uploaded images', { uploadedImages });
            }

            // Process the user message using conversational assistant
            const conversationalResponse = await this.operations.processUserMessage.execute(
                { 
                    userMessage, 
                    conversationState,
                    conversationResponseCallback: (
                        message: string,
                        conversationId: string,
                        isStreaming: boolean,
                        tool?: { name: string; status: 'start' | 'success' | 'error'; args?: Record<string, unknown> },
                        reasoning?: { delta?: string; done?: boolean }
                    ) => {
                        // Track conversationId when deep_debug starts
                        if (tool?.name === 'deep_debug' && tool.status === 'start') {
                            this.deepDebugConversationId = conversationId;
                        }
                        
                        this.broadcast(WebSocketMessageResponses.CONVERSATION_RESPONSE, {
                            message,
                            conversationId,
                            isStreaming,
                            tool,
                            reasoning,
                        });
                    },
                    errors,
                    projectUpdates,
                    images: uploadedImages
                }, 
                this.getOperationOptions()
            );

            const { conversationResponse, conversationState: newConversationState } = conversationalResponse;
            this.logger.info('User input processed successfully', {
                responseLength: conversationResponse.userResponse.length,
            });

            this.infrastructure.setConversationState(newConversationState);
        } catch (error) {
            this.logger.error('Error processing user input', error);
            throw error;
        }
    }

    /**
     * Capture browser console logs, uncaught page errors and failed
     * network requests from a real headless Chromium.
     *
     * Routing is environment-driven (see
     * `worker/services/browser-capture/factory.ts`):
     *   - prod → `@cloudflare/puppeteer` over the BROWSER binding
     *   - dev  → local Node sidecar (`npm run dev:browser`)
     *
     * Both implementations share `runCapture` so behaviour is
     * identical. Never throws on sidecar unavailability — returns a
     * result with `warning` populated so the LLM sees the situation.
     */
    public async captureBrowserConsoleLogs(opts: {
        url?: string;
        waitSeconds?: number;
        viewport?: { width: number; height: number };
        interactScript?: string;
    } = {}): Promise<BrowserConsoleCaptureResult> {
        const targetUrl = opts.url ?? await this.getBrowserPreviewURL();
        const waitSeconds = Math.min(Math.max(opts.waitSeconds ?? 5, 0), 25);
        const viewport = opts.viewport ?? { width: 1280, height: 800 };
        const client = getBrowserCaptureClient(this.env, this.logger);
        return client.captureConsoleLogs({
            url: targetUrl,
            waitSeconds,
            viewport,
            interactScript: opts.interactScript,
        });
    }

    /**
     * Capture screenshot of the given URL using Cloudflare Browser Rendering REST API.
     * Includes retry logic with blank screenshot detection.
     */
    public async captureScreenshot(
        url: string,
        viewport: { width: number; height: number } = { width: 1280, height: 720 }
    ): Promise<string> {
        if (!this.env.DB || !this.getAgentId()) {
            const error = 'Cannot capture screenshot: DB or agentId not available';
            this.logger.warn(error);
            this.broadcast(WebSocketMessageResponses.SCREENSHOT_CAPTURE_ERROR, {
                error,
                configurationError: true
            });
            throw new Error(error);
        }

        if (!url) {
            const error = 'URL is required for screenshot capture';
            this.broadcast(WebSocketMessageResponses.SCREENSHOT_CAPTURE_ERROR, {
                error,
                url,
                viewport
            });
            throw new Error(error);
        }

        this.logger.info('Capturing screenshot via REST API', { url, viewport });

        // Notify start of screenshot capture
        this.broadcast(WebSocketMessageResponses.SCREENSHOT_CAPTURE_STARTED, {
            message: `Capturing screenshot of ${url}`,
            url,
            viewport
        });

        const maxRetries = SCREENSHOT_CONFIG.MAX_RETRIES;
        let lastError: Error | null = null;
        let lastBlankReason: string | null = null;

        for (let attempt = 0; attempt <= maxRetries; attempt++) {
            try {
                // Log retry attempt
                if (attempt > 0) {
                    this.logger.info(`Screenshot retry attempt ${attempt}/${maxRetries}`, {
                        url,
                        previousBlankReason: lastBlankReason
                    });
                }

                // Capture screenshot
                const base64Screenshot = await this.executeScreenshotCapture(url, viewport);

                // Detect if screenshot is blank
                const blankDetection = detectBlankScreenshot(
                    base64Screenshot,
                    SCREENSHOT_CONFIG.MIN_FILE_SIZE,
                    SCREENSHOT_CONFIG.MIN_ENTROPY
                );

                if (blankDetection.isBlank) {
                    lastBlankReason = blankDetection.reason;
                    this.logger.warn(`Blank screenshot detected on attempt ${attempt + 1}`, {
                        reason: blankDetection.reason,
                        url
                    });

                    // If we have retries left, wait and try again
                    if (attempt < maxRetries) {
                        const delay = SCREENSHOT_CONFIG.RETRY_DELAY_BASE * Math.pow(2, attempt);
                        this.logger.info(`Waiting ${delay}ms before retry...`);
                        await new Promise(resolve => setTimeout(resolve, delay));
                        continue;
                    }

                    // On final attempt, use the screenshot anyway
                    this.logger.warn('All retry attempts resulted in blank screenshot, using last capture');
                }

                // Process and store the screenshot
                return await this.processAndStoreScreenshot(base64Screenshot, url, viewport);

            } catch (error) {
                lastError = error instanceof Error ? error : new Error(String(error));
                this.logger.error(`Screenshot capture attempt ${attempt + 1} failed:`, error);

                // If we have retries left, wait and try again
                if (attempt < maxRetries) {
                    const delay = SCREENSHOT_CONFIG.RETRY_DELAY_BASE * Math.pow(2, attempt);
                    await new Promise(resolve => setTimeout(resolve, delay));
                }
            }
        }

        // All attempts failed
        const errorMessage = lastError?.message || lastBlankReason || 'Unknown error after retries';
        this.broadcast(WebSocketMessageResponses.SCREENSHOT_CAPTURE_ERROR, {
            error: `Screenshot capture failed after ${maxRetries + 1} attempts: ${errorMessage}`,
            url,
            viewport
        });
        throw new Error(`Screenshot capture failed: ${errorMessage}`);
    }

    /**
     * Execute a single screenshot capture attempt using Cloudflare Browser Rendering API.
     */
    private async executeScreenshotCapture(
        url: string,
        viewport: { width: number; height: number }
    ): Promise<string> {
        const apiUrl = `https://api.cloudflare.com/client/v4/accounts/${this.env.CLOUDFLARE_ACCOUNT_ID}/browser-rendering/snapshot`;

        const response = await fetch(apiUrl, {
            method: 'POST',
            headers: {
                'Authorization': `Bearer ${this.env.CLOUDFLARE_API_TOKEN}`,
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                url: url,
                viewport: viewport,
                gotoOptions: {
                    waitUntil: 'networkidle2',
                    timeout: SCREENSHOT_CONFIG.PAGE_LOAD_TIMEOUT
                },
                waitForTimeout: SCREENSHOT_CONFIG.WAIT_FOR_TIMEOUT,
                screenshotOptions: {
                    fullPage: false,
                    type: 'png'
                }
            }),
        });

        if (!response.ok) {
            const errorText = await response.text();
            throw new Error(`Browser Rendering API failed: ${response.status} - ${errorText}`);
        }

        const result = await response.json() as {
            success: boolean;
            result: {
                screenshot: string;
                content: string;
            };
        };

        if (!result.success || !result.result.screenshot) {
            throw new Error('Browser Rendering API succeeded but no screenshot returned');
        }

        return result.result.screenshot;
    }

    /**
     * Process and store a captured screenshot.
     */
    private async processAndStoreScreenshot(
        base64Screenshot: string,
        url: string,
        viewport: { width: number; height: number }
    ): Promise<string> {
        const screenshot: ImageAttachment = {
            id: this.getAgentId(),
            filename: 'latest.png',
            mimeType: 'image/png',
            base64Data: base64Screenshot
        };
        const uploadedImage = await uploadImage(this.env, screenshot, ImageType.SCREENSHOTS);

        // Persist in database
        try {
            const appService = new AppService(this.env);
            await appService.updateAppScreenshot(this.getAgentId(), uploadedImage.publicUrl);
        } catch (dbError) {
            const error = `Database update failed: ${dbError instanceof Error ? dbError.message : 'Unknown database error'}`;
            this.broadcast(WebSocketMessageResponses.SCREENSHOT_CAPTURE_ERROR, {
                error,
                url,
                viewport,
                screenshotCaptured: true,
                databaseError: true
            });
            throw new Error(error);
        }

        this.logger.info('Screenshot captured and stored successfully', {
            url,
            storage: uploadedImage.publicUrl.startsWith('data:') ? 'database' : (uploadedImage.publicUrl.includes('/api/screenshots/') ? 'r2' : 'images'),
            length: base64Screenshot.length
        });

        // Sign the URL if it points to our internal screenshot endpoint
        const security = new ScreenshotSecurity(this.env);
        const signedUrl = await security.signUrl(uploadedImage.publicUrl, this.getAgentId());

        // Notify successful screenshot capture
        this.broadcast(WebSocketMessageResponses.SCREENSHOT_CAPTURE_SUCCESS, {
            message: `Successfully captured screenshot of ${url}`,
            url,
            viewport,
            screenshotSize: base64Screenshot.length,
            timestamp: new Date().toISOString(),
            screenshotUrl: signedUrl,
        });

        return signedUrl;
    }
}