fix(auto-reply): fall back to json when rpc prompt empty

This commit is contained in:
Peter Steinberger
2025-12-10 14:58:03 +00:00
parent f6a86e5527
commit 4db69c8eac

View File

@@ -1,3 +1,4 @@
import { spawn } from "node:child_process";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
@@ -80,6 +81,55 @@ function stripRpcNoise(raw: string): string {
return kept.join("\n");
}
async function runJsonFallback(opts: {
argv: string[];
cwd?: string;
timeoutMs: number;
}): Promise<{
stdout: string;
stderr: string;
code: number;
signal?: NodeJS.Signals | null;
killed?: boolean;
}> {
return await new Promise((resolve, reject) => {
const child = spawn(opts.argv[0], opts.argv.slice(1), {
cwd: opts.cwd,
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
const timer = setTimeout(() => {
child.kill("SIGKILL");
reject(
new Error(
`pi json fallback timed out after ${Math.round(opts.timeoutMs / 1000)}s`,
),
);
}, opts.timeoutMs);
child.stdout.on("data", (d) => {
stdout += d.toString();
});
child.stderr.on("data", (d) => {
stderr += d.toString();
});
child.on("error", (err) => {
clearTimeout(timer);
reject(err);
});
child.on("exit", (code, signal) => {
clearTimeout(timer);
resolve({
stdout,
stderr,
code: code ?? 0,
signal,
killed: child.killed,
});
});
});
}
function extractRpcAssistantText(raw: string): string | undefined {
if (!raw.trim()) return undefined;
let deltaBuffer = "";
@@ -561,14 +611,34 @@ export async function runCommandReply(
streamedAny = true;
};
const preferRpc = process.env.CLAWDIS_USE_PI_RPC === "1";
const run = async () => {
const runId = params.runId ?? crypto.randomUUID();
const rpcPromptIndex =
promptIndex >= 0 ? promptIndex : finalArgv.length - 1;
let body = promptArg ?? "";
if (!body || !body.trim()) {
body = templatingCtx.Body ?? templatingCtx.BodyStripped ?? "";
}
if (!preferRpc) {
const jsonArgv = (() => {
const copy = [...finalArgv];
const idx = copy.indexOf("--mode");
if (idx >= 0 && copy[idx + 1]) copy[idx + 1] = "json";
else copy.push("--mode", "json");
return copy;
})();
logVerbose(
`Running command auto-reply in json mode: ${jsonArgv.join(" ")}${reply.cwd ? ` (cwd: ${reply.cwd})` : ""}`,
);
return await runJsonFallback({
argv: jsonArgv,
cwd: reply.cwd,
timeoutMs,
});
}
const rpcPromptIndex =
promptIndex >= 0 ? promptIndex : finalArgv.length - 1;
logVerbose(
`pi rpc prompt (${body.length} chars): ${body.slice(0, 200).replace(/\n/g, "\\n")}`,
);
@@ -735,12 +805,52 @@ export async function runCommandReply(
}
},
});
const rawStdout = stdout.trim();
const rpcAssistantText = extractRpcAssistantText(stdout);
let stdoutUsed = stdout;
let stderrUsed = stderr;
let codeUsed = code;
let signalUsed = signal;
let killedUsed = killed;
let rpcAssistantText = extractRpcAssistantText(stdoutUsed);
let rawStdout = stdoutUsed.trim();
const _rpcUserEmpty =
/"role":"user","content":\[\{"type":"text","text":""\}\]/.test(rawStdout);
const anthropicNoMessages = rawStdout.includes(
"messages: at least one message is required",
);
const shouldRetryJson =
preferRpc && body.trim().length > 0 && anthropicNoMessages;
if (shouldRetryJson) {
const jsonArgv = (() => {
const copy = [...finalArgv];
const idx = copy.indexOf("--mode");
if (idx >= 0 && copy[idx + 1]) copy[idx + 1] = "json";
else copy.push("--mode", "json");
return copy;
})();
logVerbose(
`pi rpc returned empty user text; retrying with json mode: ${jsonArgv.join(" ")}`,
);
try {
const fallback = await runJsonFallback({
argv: jsonArgv,
cwd: reply.cwd,
timeoutMs,
});
stdoutUsed = fallback.stdout;
stderrUsed = fallback.stderr;
codeUsed = fallback.code;
signalUsed = fallback.signal ?? null;
killedUsed = fallback.killed;
rpcAssistantText = extractRpcAssistantText(stdoutUsed);
rawStdout = stdoutUsed.trim();
} catch (err) {
logVerbose(`json fallback failed: ${String(err)}`);
}
}
let mediaFromCommand: string[] | undefined;
const trimmed = stripRpcNoise(rawStdout);
if (stderr?.trim()) {
logVerbose(`Command auto-reply stderr: ${stderr.trim()}`);
if (stderrUsed?.trim()) {
logVerbose(`Command auto-reply stderr: ${stderrUsed.trim()}`);
}
const logFailure = () => {
@@ -748,13 +858,13 @@ export async function runCommandReply(
s ? (s.length > 4000 ? `${s.slice(0, 4000)}` : s) : undefined;
logger.warn(
{
code,
signal,
killed,
code: codeUsed,
signal: signalUsed,
killed: killedUsed,
argv: finalArgv,
cwd: reply.cwd,
stdout: truncate(rawStdout),
stderr: truncate(stderr),
stderr: truncate(stderrUsed),
},
"command auto-reply failed",
);
@@ -885,44 +995,44 @@ export async function runCommandReply(
{ durationMs: elapsed, agent: agentKind, cwd: reply.cwd },
"command auto-reply finished",
);
if ((code ?? 0) !== 0) {
if ((codeUsed ?? 0) !== 0) {
logFailure();
console.error(
`Command auto-reply exited with code ${code ?? "unknown"} (signal: ${signal ?? "none"})`,
`Command auto-reply exited with code ${codeUsed ?? "unknown"} (signal: ${signalUsed ?? "none"})`,
);
// Include any partial output or stderr in error message
const summarySource = rpcAssistantText ?? trimmed;
const partialOut = summarySource
? `\n\nOutput: ${summarySource.slice(0, 500)}${summarySource.length > 500 ? "..." : ""}`
: "";
const errorText = `⚠️ Command exited with code ${code ?? "unknown"}${signal ? ` (${signal})` : ""}${partialOut}`;
const errorText = `⚠️ Command exited with code ${codeUsed ?? "unknown"}${signalUsed ? ` (${signalUsed})` : ""}${partialOut}`;
return {
payloads: [{ text: errorText }],
meta: {
durationMs: Date.now() - started,
queuedMs,
queuedAhead,
exitCode: code,
signal,
killed,
exitCode: codeUsed,
signal: signalUsed,
killed: killedUsed,
agentMeta: parsed?.meta,
},
};
}
if (killed && !signal) {
if (killedUsed && !signalUsed) {
console.error(
`Command auto-reply process killed before completion (exit code ${code ?? "unknown"})`,
`Command auto-reply process killed before completion (exit code ${codeUsed ?? "unknown"})`,
);
const errorText = `⚠️ Command was killed before completion (exit code ${code ?? "unknown"})`;
const errorText = `⚠️ Command was killed before completion (exit code ${codeUsed ?? "unknown"})`;
return {
payloads: [{ text: errorText }],
meta: {
durationMs: Date.now() - started,
queuedMs,
queuedAhead,
exitCode: code,
signal,
killed,
exitCode: codeUsed,
signal: signalUsed,
killed: killedUsed,
agentMeta: parsed?.meta,
},
};
@@ -931,9 +1041,9 @@ export async function runCommandReply(
durationMs: Date.now() - started,
queuedMs,
queuedAhead,
exitCode: code,
signal,
killed,
exitCode: codeUsed,
signal: signalUsed,
killed: killedUsed,
agentMeta: parsed?.meta,
};