fix(auto-reply): fall back to json when rpc prompt empty
This commit is contained in:
@@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user