chore(telemetry): add path-confusion loop breaker and strict blank-preview diagnostic protocol

This commit is contained in:
2026-06-09 16:27:09 -07:00
parent 472e30e9bc
commit 7ddcbfe32d

View File

@@ -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 previewUrl host, no protocol>' } }\`. 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<string, string>();
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<string>();
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;
}