fix(runner): fix fingerprint collision on projectId and relax loop-break limits

This commit is contained in:
2026-06-03 13:58:56 -07:00
parent d7206ea2ee
commit fa8a919214
2 changed files with 64 additions and 37 deletions

View File

@@ -300,21 +300,30 @@ File: "${path.relative(opts.repoRoot ?? ctx.workspaceRoot, task.filePath)}" (lin
const toolFingerprints = []; const toolFingerprints = [];
let ralphIteration = 0; let ralphIteration = 0;
function fingerprintToolCall(tc) { function fingerprintToolCall(tc) {
if (tc.name === "shell_exec") { const name = tc.name;
const cmd = String(tc.args?.command ?? "").trim(); const args = tc.args ?? {};
const verb = cmd if (name === "shell_exec") {
.split("&&") const cmd = String(args.command ?? "").trim();
.map((s) => s.trim()) const firstWord = cmd.split(/\s+/)[0] ?? "shell";
.find((s) => !s.startsWith("cd ")) return `shell_exec:${firstWord}`;
?.split(/\s+/)[0] ?? "shell";
return `shell_exec:${verb}`;
} }
if (tc.name === "fs_write" || // Determine target based on most common descriptive parameter keys
tc.name === "fs_edit" || const target = args.path ??
tc.name === "fs_read") { args.pattern ??
return `${tc.name}:${tc.args?.path}`; args.command ??
args.commandId ??
args.appUuid ??
args.uuid ??
"";
if (target) {
return `${name}:${target}`;
} }
return `${tc.name}:${Object.values(tc.args ?? {})[0]}`; // Filter out common metadata like projectId, and use first real argument
const keys = Object.keys(args).filter((k) => k !== "projectId");
if (keys.length > 0) {
return `${name}:${args[keys[0]]}`;
}
return `${name}:default`;
} }
while (subTurn < SUB_MAX_TURNS) { while (subTurn < SUB_MAX_TURNS) {
if (opts.isStopped()) { if (opts.isStopped()) {
@@ -421,20 +430,23 @@ ${verification.error}
for (const tc of resp.toolCalls) { for (const tc of resp.toolCalls) {
toolFingerprints.push(fingerprintToolCall(tc)); toolFingerprints.push(fingerprintToolCall(tc));
} }
const window = toolFingerprints.slice(-6); const window = toolFingerprints.slice(-12);
const counts = new Map(); const counts = new Map();
for (const fp of window) for (const fp of window)
counts.set(fp, (counts.get(fp) ?? 0) + 1); counts.set(fp, (counts.get(fp) ?? 0) + 1);
let maxRepeats = 0; let maxRepeats = 0;
let repeatedCmd = "";
for (const [fp, n] of counts.entries()) { for (const [fp, n] of counts.entries()) {
if (n > maxRepeats) if (n > maxRepeats) {
maxRepeats = n; maxRepeats = n;
repeatedCmd = fp;
}
} }
if (maxRepeats >= 4) { if (maxRepeats >= 6) {
await emit({ await emit({
ts: now(), ts: now(),
type: "error", type: "error",
text: `Loop detected in subtask execution, breaking loop.`, text: `Loop detected in subtask execution (repeated "${repeatedCmd}" ${maxRepeats}x in last 12 calls), breaking loop.`,
}); });
return false; return false;
} }

View File

@@ -403,24 +403,35 @@ File: "${path.relative(opts.repoRoot ?? ctx.workspaceRoot, task.filePath)}" (lin
let ralphIteration = 0; let ralphIteration = 0;
function fingerprintToolCall(tc: any) { function fingerprintToolCall(tc: any) {
if (tc.name === "shell_exec") { const name = tc.name;
const cmd = String(tc.args?.command ?? "").trim(); const args = tc.args ?? {};
const verb =
cmd if (name === "shell_exec") {
.split("&&") const cmd = String(args.command ?? "").trim();
.map((s) => s.trim()) const firstWord = cmd.split(/\s+/)[0] ?? "shell";
.find((s) => !s.startsWith("cd ")) return `shell_exec:${firstWord}`;
?.split(/\s+/)[0] ?? "shell";
return `shell_exec:${verb}`;
} }
if (
tc.name === "fs_write" || // Determine target based on most common descriptive parameter keys
tc.name === "fs_edit" || const target =
tc.name === "fs_read" args.path ??
) { args.pattern ??
return `${tc.name}:${tc.args?.path}`; args.command ??
args.commandId ??
args.appUuid ??
args.uuid ??
"";
if (target) {
return `${name}:${target}`;
} }
return `${tc.name}:${Object.values(tc.args ?? {})[0]}`;
// Filter out common metadata like projectId, and use first real argument
const keys = Object.keys(args).filter((k) => k !== "projectId");
if (keys.length > 0) {
return `${name}:${args[keys[0]]}`;
}
return `${name}:default`;
} }
while (subTurn < SUB_MAX_TURNS) { while (subTurn < SUB_MAX_TURNS) {
@@ -536,20 +547,24 @@ ${verification.error}
for (const tc of resp.toolCalls) { for (const tc of resp.toolCalls) {
toolFingerprints.push(fingerprintToolCall(tc)); toolFingerprints.push(fingerprintToolCall(tc));
} }
const window = toolFingerprints.slice(-6); const window = toolFingerprints.slice(-12);
const counts = new Map<string, number>(); const counts = new Map<string, number>();
for (const fp of window) counts.set(fp, (counts.get(fp) ?? 0) + 1); for (const fp of window) counts.set(fp, (counts.get(fp) ?? 0) + 1);
let maxRepeats = 0; let maxRepeats = 0;
let repeatedCmd = "";
for (const [fp, n] of counts.entries()) { for (const [fp, n] of counts.entries()) {
if (n > maxRepeats) maxRepeats = n; if (n > maxRepeats) {
maxRepeats = n;
repeatedCmd = fp;
}
} }
if (maxRepeats >= 4) { if (maxRepeats >= 6) {
await emit({ await emit({
ts: now(), ts: now(),
type: "error", type: "error",
text: `Loop detected in subtask execution, breaking loop.`, text: `Loop detected in subtask execution (repeated "${repeatedCmd}" ${maxRepeats}x in last 12 calls), breaking loop.`,
}); });
return false; return false;
} }