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 fs from "node:fs/promises";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
@@ -80,6 +81,55 @@ function stripRpcNoise(raw: string): string {
return kept.join("\n"); 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 { function extractRpcAssistantText(raw: string): string | undefined {
if (!raw.trim()) return undefined; if (!raw.trim()) return undefined;
let deltaBuffer = ""; let deltaBuffer = "";
@@ -561,14 +611,34 @@ export async function runCommandReply(
streamedAny = true; streamedAny = true;
}; };
const preferRpc = process.env.CLAWDIS_USE_PI_RPC === "1";
const run = async () => { const run = async () => {
const runId = params.runId ?? crypto.randomUUID(); const runId = params.runId ?? crypto.randomUUID();
const rpcPromptIndex =
promptIndex >= 0 ? promptIndex : finalArgv.length - 1;
let body = promptArg ?? ""; let body = promptArg ?? "";
if (!body || !body.trim()) { if (!body || !body.trim()) {
body = templatingCtx.Body ?? templatingCtx.BodyStripped ?? ""; 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( logVerbose(
`pi rpc prompt (${body.length} chars): ${body.slice(0, 200).replace(/\n/g, "\\n")}`, `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(); let stdoutUsed = stdout;
const rpcAssistantText = extractRpcAssistantText(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; let mediaFromCommand: string[] | undefined;
const trimmed = stripRpcNoise(rawStdout); const trimmed = stripRpcNoise(rawStdout);
if (stderr?.trim()) { if (stderrUsed?.trim()) {
logVerbose(`Command auto-reply stderr: ${stderr.trim()}`); logVerbose(`Command auto-reply stderr: ${stderrUsed.trim()}`);
} }
const logFailure = () => { const logFailure = () => {
@@ -748,13 +858,13 @@ export async function runCommandReply(
s ? (s.length > 4000 ? `${s.slice(0, 4000)}` : s) : undefined; s ? (s.length > 4000 ? `${s.slice(0, 4000)}` : s) : undefined;
logger.warn( logger.warn(
{ {
code, code: codeUsed,
signal, signal: signalUsed,
killed, killed: killedUsed,
argv: finalArgv, argv: finalArgv,
cwd: reply.cwd, cwd: reply.cwd,
stdout: truncate(rawStdout), stdout: truncate(rawStdout),
stderr: truncate(stderr), stderr: truncate(stderrUsed),
}, },
"command auto-reply failed", "command auto-reply failed",
); );
@@ -885,44 +995,44 @@ export async function runCommandReply(
{ durationMs: elapsed, agent: agentKind, cwd: reply.cwd }, { durationMs: elapsed, agent: agentKind, cwd: reply.cwd },
"command auto-reply finished", "command auto-reply finished",
); );
if ((code ?? 0) !== 0) { if ((codeUsed ?? 0) !== 0) {
logFailure(); logFailure();
console.error( 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 // Include any partial output or stderr in error message
const summarySource = rpcAssistantText ?? trimmed; const summarySource = rpcAssistantText ?? trimmed;
const partialOut = summarySource const partialOut = summarySource
? `\n\nOutput: ${summarySource.slice(0, 500)}${summarySource.length > 500 ? "..." : ""}` ? `\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 { return {
payloads: [{ text: errorText }], payloads: [{ text: errorText }],
meta: { meta: {
durationMs: Date.now() - started, durationMs: Date.now() - started,
queuedMs, queuedMs,
queuedAhead, queuedAhead,
exitCode: code, exitCode: codeUsed,
signal, signal: signalUsed,
killed, killed: killedUsed,
agentMeta: parsed?.meta, agentMeta: parsed?.meta,
}, },
}; };
} }
if (killed && !signal) { if (killedUsed && !signalUsed) {
console.error( 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 { return {
payloads: [{ text: errorText }], payloads: [{ text: errorText }],
meta: { meta: {
durationMs: Date.now() - started, durationMs: Date.now() - started,
queuedMs, queuedMs,
queuedAhead, queuedAhead,
exitCode: code, exitCode: codeUsed,
signal, signal: signalUsed,
killed, killed: killedUsed,
agentMeta: parsed?.meta, agentMeta: parsed?.meta,
}, },
}; };
@@ -931,9 +1041,9 @@ export async function runCommandReply(
durationMs: Date.now() - started, durationMs: Date.now() - started,
queuedMs, queuedMs,
queuedAhead, queuedAhead,
exitCode: code, exitCode: codeUsed,
signal, signal: signalUsed,
killed, killed: killedUsed,
agentMeta: parsed?.meta, agentMeta: parsed?.meta,
}; };