diff --git a/vibn-frontend/app/api/chat/route.ts b/vibn-frontend/app/api/chat/route.ts index 0913952e..703ed7d8 100644 --- a/vibn-frontend/app/api/chat/route.ts +++ b/vibn-frontend/app/api/chat/route.ts @@ -317,7 +317,8 @@ If the user tells you the preview is blank, not loading, or shows nothing: 3. If it is not running, run \`dev_server_start\`. 4. If it is running, run \`dev_server_logs\` on its port to check for compilation hangs (e.g. Turbopack slow filesystem hangs) or fatal errors. 5. Run \`browser_console\` on the previewUrl. -6. ONLY edit code or configuration once the logs/console explicitly identify the source file or error. +6. Check \`shell_exec { command: "curl -sI http://localhost:3000" }\` to verify if the server is responding locally (bypassing the proxy). +7. ONLY edit code or configuration once the logs/console explicitly identify the source file or error. **HMR through the proxy (apply when scaffolding):** - **Vite (verified working):** in \`vite.config\` set \`server: { host: '0.0.0.0', port: <3000-3009>, strictPort: true, hmr: { clientPort: 443, protocol: 'wss', host: '' } }\`. The \`hmr.host\` is REQUIRED — without it Vite's HMR client can guess the wrong host and the WS handshake fails through Traefik. Default localhost binding looks fine locally but breaks HMR through the proxy. @@ -832,6 +833,7 @@ export async function POST(request: Request) { let lastVerifySig: string | null = null; let lastRoundToolSig: string | null = null; let lastRoundResults: any[] = []; + let lastRoundToolCalls: any[] = []; let fileHashes = new Map(); let stallRounds = 0; @@ -1077,6 +1079,8 @@ export async function POST(request: Request) { const pathConfusion = detectPathConfusion( currentRoundResults, lastRoundResults, + resp.toolCalls, + lastRoundToolCalls, ); if (pathConfusion) { loopBreakReason = `PATH_CONFUSION: ${pathConfusion}`; @@ -1084,6 +1088,7 @@ export async function POST(request: Request) { lastRoundToolSig = currentRoundToolSig; lastRoundResults = currentRoundResults; + lastRoundToolCalls = resp.toolCalls; if (loopBreakReason) break; } @@ -1227,13 +1232,28 @@ export async function POST(request: Request) { const toolResults = messages.filter((m) => m.role === "tool"); finalMsg._rawToolResults = assistantToolCalls.map((tc) => { const tr = toolResults.find((m) => m.toolCallId === tc.id); + let resultStr = + typeof tr?.content === "string" + ? tr.content + : JSON.stringify(tr?.content || ""); + + // Redact secrets from telemetry + resultStr = resultStr.replace( + /postgres(?:ql)?:\/\/[^:]+:[^@]+@[^:]+:\d+\/[^\s"]+/g, + "postgresql://[REDACTED_DB_URL]", + ); + resultStr = resultStr.replace( + /(eyJ[a-zA-Z0-9_-]{5,}\.[a-zA-Z0-9_-]{5,}\.[a-zA-Z0-9_-]{5,})/g, + "[REDACTED_JWT]", + ); + resultStr = resultStr.replace(/([A-Za-z0-9_]{35,})/g, (match) => + match.length > 40 ? "[REDACTED_SECRET]" : match, + ); + return { name: tc.name, args: tc.args, - result: - typeof tr?.content === "string" - ? tr.content - : JSON.stringify(tr?.content || ""), + result: resultStr, }; }); } @@ -1566,43 +1586,71 @@ function checkRoundProgress( return { progressed, nextHashes }; } -function detectPathConfusion(current: any[], last: any[]): string | null { - const missingFiles = new Set(); +function safeJson(str: string) { + try { + return JSON.parse(str); + } catch { + return null; + } +} - const extractMissingFiles = (results: any[]) => { - for (const tr of results) { - if (!tr.content) continue; - try { - const parsed = JSON.parse(tr.content); - const resultStr = - typeof parsed === "string" ? parsed : JSON.stringify(parsed); - const argsStr = typeof tr.toolName === "string" ? tr.toolName : ""; - const allStr = resultStr + argsStr; +type PathFailure = { + tool: string; + attemptedPath?: string; + basename?: string; + error: string; +}; - if ( - allStr.includes("not a file or missing") || - allStr.includes("No such file or directory") - ) { - const match = allStr.match(/([a-zA-Z0-9_\-\.]+\.[a-zA-Z0-9]+)/); - if (match && match[1]) { - missingFiles.add(match[1]); - } - } - } catch (e) {} +function extractPathFailures(results: any[], toolCalls: any[]): PathFailure[] { + const failures: PathFailure[] = []; + + for (const tr of results) { + const content = String(tr.content ?? ""); + if ( + !content.includes("not a file or missing") && + !content.includes("No such file or directory") && + !content.includes("ENOENT") && + !content.includes("Could not read file") + ) { + continue; } - }; - extractMissingFiles(current); - extractMissingFiles(last); + const tc = toolCalls.find((t: any) => t.id === tr.toolCallId); + // Attempt to extract the path from the tool call args first, then regex fallback + const attempted = + tc?.args?.path || + tc?.args?.command?.match(/cat\s+([^\s]+)/)?.[1] || + content.match(/(?:for|open|read file|access)\s+'?([^':\s]+)/)?.[1]; - // If the agent hit a "Not a file" error on the same basename across two different rounds - if (missingFiles.size > 0 && current.length > 0 && last.length > 0) { - for (const file of missingFiles) { - // Very basic heuristic: if it complains about the same file twice, break the loop - // and instruct it to use \`find\` - return `You are trying to read ${file} but the path is wrong. Stop guessing paths. Run 'shell_exec { command: "find . -name ${file}" }' to discover the exact path to this file, then use it exactly once.`; + if (attempted) { + failures.push({ + tool: tr.toolName, + attemptedPath: attempted, + basename: attempted.split("/").pop(), + error: content.slice(0, 300), + }); } } + return failures; +} +function detectPathConfusion( + currentResults: any[], + lastResults: any[], + currentToolCalls: any[], + lastToolCalls: any[], +): string | null { + const currentFailures = extractPathFailures(currentResults, currentToolCalls); + const lastFailures = extractPathFailures(lastResults, lastToolCalls); + + if (currentFailures.length > 0 && lastFailures.length > 0) { + for (const cf of currentFailures) { + for (const lf of lastFailures) { + if (cf.basename && cf.basename === lf.basename) { + return `You are in a path-confusion loop trying to access ${cf.basename}. Stop reading guessed paths. Run 'shell_exec { command: "find . -name ${cf.basename}" }' to discover the exact path, then use it exactly once.`; + } + } + } + } return null; }