chore(telemetry): add path-confusion loop breaker and strict blank-preview diagnostic protocol
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user