232 lines
8.6 KiB
JavaScript
Executable File
232 lines
8.6 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const os = require('node:os');
|
|
|
|
function resolveOpenClawPackageRoot() {
|
|
const wrapperPath = path.join(os.homedir(), '.local', 'bin', 'openclaw');
|
|
const wrapper = fs.readFileSync(wrapperPath, 'utf8');
|
|
const match = wrapper.match(/"([^"]*node_modules\/openclaw)\/openclaw\.mjs"/);
|
|
if (!match) throw new Error(`Could not resolve openclaw package root from ${wrapperPath}`);
|
|
const raw = match[1];
|
|
if (raw.startsWith('$basedir/')) {
|
|
return path.resolve(path.dirname(wrapperPath), raw.replace(/^\$basedir\//, ''));
|
|
}
|
|
return raw;
|
|
}
|
|
|
|
function replaceOnce(content, oldText, newText, label, filePath) {
|
|
if (content.includes(newText)) return { content, changed: false, already: true };
|
|
if (!content.includes(oldText)) {
|
|
throw new Error(`Patch block not found for ${label} in ${filePath}`);
|
|
}
|
|
return { content: content.replace(oldText, newText), changed: true, already: false };
|
|
}
|
|
|
|
function ensureDir(dir) {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
}
|
|
|
|
const extractAssistantTextOld = `function extractAssistantText(message) {
|
|
\tif (!message || typeof message !== "object") return;
|
|
\tif (message.role !== "assistant") return;
|
|
\tconst content = message.content;
|
|
\tif (!Array.isArray(content)) return;
|
|
\tconst joined = extractTextFromChatContent(content, {
|
|
\t\tsanitizeText: sanitizeTextContent,
|
|
\t\tjoinWith: "",
|
|
\t\tnormalizeText: (text) => text.trim()
|
|
\t}) ?? "";
|
|
\tconst stopReason = message.stopReason;
|
|
\tconst errorMessage = message.errorMessage;
|
|
\tconst errorContext = stopReason === "error" || typeof errorMessage === "string" && Boolean(errorMessage.trim());
|
|
\treturn joined ? sanitizeUserFacingText(joined, { errorContext }) : void 0;
|
|
}`;
|
|
|
|
const extractAssistantTextNew = `function extractAssistantText(message) {
|
|
\tif (!message || typeof message !== "object") return;
|
|
\tif (message.role !== "assistant") return;
|
|
\tconst content = message.content;
|
|
\tif (!Array.isArray(content)) return;
|
|
\tconst joined = extractTextFromChatContent(content, {
|
|
\t\tsanitizeText: sanitizeTextContent,
|
|
\t\tjoinWith: "",
|
|
\t\tnormalizeText: (text) => text.trim()
|
|
\t}) ?? "";
|
|
\tconst stopReason = message.stopReason;
|
|
\tconst errorMessage = message.errorMessage;
|
|
\tconst errorContext = stopReason === "error" || typeof errorMessage === "string" && Boolean(errorMessage.trim());
|
|
\treturn joined ? sanitizeUserFacingText(joined, { errorContext }) : void 0;
|
|
}
|
|
function extractAssistantTerminalText(message) {
|
|
\tif (!message || typeof message !== "object") return { isError: false };
|
|
\tif (message.role !== "assistant") return { isError: false };
|
|
\tconst stopReason = message.stopReason;
|
|
\tconst rawErrorMessage = message.errorMessage;
|
|
\tconst isError = stopReason === "error" || typeof rawErrorMessage === "string" && Boolean(rawErrorMessage.trim());
|
|
\tconst text = extractAssistantText(message);
|
|
\tif (text?.trim()) return { text, isError };
|
|
\tif (typeof rawErrorMessage === "string" && rawErrorMessage.trim()) {
|
|
\t\treturn {
|
|
\t\t\ttext: sanitizeUserFacingText(rawErrorMessage.trim(), { errorContext: true }),
|
|
\t\t\tisError: true
|
|
\t\t};
|
|
\t}
|
|
\treturn { isError };
|
|
}`;
|
|
|
|
const readLatestAssistantReplyOld = `async function readLatestAssistantReply(params) {
|
|
\tconst history = await callGateway({
|
|
\t\tmethod: "chat.history",
|
|
\t\tparams: {
|
|
\t\t\tsessionKey: params.sessionKey,
|
|
\t\t\tlimit: params.limit ?? 50
|
|
\t\t}
|
|
\t});
|
|
\tconst filtered = stripToolMessages(Array.isArray(history?.messages) ? history.messages : []);
|
|
\tfor (let i = filtered.length - 1; i >= 0; i -= 1) {
|
|
\t\tconst candidate = filtered[i];
|
|
\t\tif (!candidate || typeof candidate !== "object") continue;
|
|
\t\tif (candidate.role !== "assistant") continue;
|
|
\t\tconst text = extractAssistantText(candidate);
|
|
\t\tif (!text?.trim()) continue;
|
|
\t\treturn text;
|
|
\t}
|
|
}`;
|
|
|
|
const readLatestAssistantReplyNew = `async function readLatestAssistantOutcome(params) {
|
|
\tconst history = await callGateway({
|
|
\t\tmethod: "chat.history",
|
|
\t\tparams: {
|
|
\t\t\tsessionKey: params.sessionKey,
|
|
\t\t\tlimit: params.limit ?? 50
|
|
\t\t}
|
|
\t});
|
|
\tconst filtered = stripToolMessages(Array.isArray(history?.messages) ? history.messages : []);
|
|
\tfor (let i = filtered.length - 1; i >= 0; i -= 1) {
|
|
\t\tconst candidate = filtered[i];
|
|
\t\tif (!candidate || typeof candidate !== "object") continue;
|
|
\t\tif (candidate.role !== "assistant") continue;
|
|
\t\treturn extractAssistantTerminalText(candidate);
|
|
\t}
|
|
\treturn { isError: false };
|
|
}
|
|
async function readLatestAssistantReply(params) {
|
|
\tconst outcome = await readLatestAssistantOutcome(params);
|
|
\treturn outcome.text?.trim() ? outcome.text : void 0;
|
|
}`;
|
|
|
|
const waitOutcomeOld = `\t\tconst waitError = typeof wait.error === "string" ? wait.error : void 0;
|
|
\t\tconst outcome = wait.status === "error" ? {
|
|
\t\t\tstatus: "error",
|
|
\t\t\terror: waitError
|
|
\t\t} : wait.status === "timeout" ? { status: "timeout" } : { status: "ok" };
|
|
\t\tif (!runOutcomesEqual(entry.outcome, outcome)) {
|
|
\t\t\tentry.outcome = outcome;
|
|
\t\t\tmutated = true;
|
|
\t\t}
|
|
\t\tif (mutated) persistSubagentRuns();
|
|
\t\tawait completeSubagentRun({
|
|
\t\t\trunId,
|
|
\t\t\tendedAt: entry.endedAt,
|
|
\t\t\toutcome,
|
|
\t\t\treason: wait.status === "error" ? SUBAGENT_ENDED_REASON_ERROR : SUBAGENT_ENDED_REASON_COMPLETE,
|
|
\t\t\tsendFarewell: true,
|
|
\t\t\taccountId: entry.requesterOrigin?.accountId,
|
|
\t\t\ttriggerCleanup: true
|
|
\t\t});`;
|
|
|
|
const waitOutcomeNew = `\t\tconst waitError = typeof wait.error === "string" ? wait.error : void 0;
|
|
\t\tlet outcome = wait.status === "error" ? {
|
|
\t\t\tstatus: "error",
|
|
\t\t\terror: waitError
|
|
\t\t} : wait.status === "timeout" ? { status: "timeout" } : { status: "ok" };
|
|
\t\tif (outcome.status === "ok") try {
|
|
\t\t\tconst latestAssistant = await readLatestAssistantOutcome({
|
|
\t\t\t\tsessionKey: entry.childSessionKey,
|
|
\t\t\t\tlimit: 50
|
|
\t\t\t});
|
|
\t\t\tif (latestAssistant.isError) outcome = {
|
|
\t\t\t\tstatus: "error",
|
|
\t\t\t\terror: latestAssistant.text?.trim() || waitError
|
|
\t\t\t};
|
|
\t\t} catch {}
|
|
\t\tif (!runOutcomesEqual(entry.outcome, outcome)) {
|
|
\t\t\tentry.outcome = outcome;
|
|
\t\t\tmutated = true;
|
|
\t\t}
|
|
\t\tif (mutated) persistSubagentRuns();
|
|
\t\tawait completeSubagentRun({
|
|
\t\t\trunId,
|
|
\t\t\tendedAt: entry.endedAt,
|
|
\t\t\toutcome,
|
|
\t\t\treason: outcome.status === "error" ? SUBAGENT_ENDED_REASON_ERROR : SUBAGENT_ENDED_REASON_COMPLETE,
|
|
\t\t\tsendFarewell: true,
|
|
\t\t\taccountId: entry.requesterOrigin?.accountId,
|
|
\t\t\ttriggerCleanup: true
|
|
\t\t});`;
|
|
|
|
const announceGuardOld = `\t\tif (!outcome) outcome = { status: "unknown" };`;
|
|
const announceGuardNew = `\t\tif (outcome?.status === "ok") try {
|
|
\t\t\tconst latestAssistant = await readLatestAssistantOutcome({
|
|
\t\t\t\tsessionKey: params.childSessionKey,
|
|
\t\t\t\tlimit: 50
|
|
\t\t\t});
|
|
\t\t\tif (latestAssistant.isError) {
|
|
\t\t\t\tif (!reply?.trim() && latestAssistant.text?.trim()) reply = latestAssistant.text;
|
|
\t\t\t\toutcome = {
|
|
\t\t\t\t\tstatus: "error",
|
|
\t\t\t\t\terror: latestAssistant.text?.trim() || outcome.error
|
|
\t\t\t\t};
|
|
\t\t\t}
|
|
\t\t} catch {}
|
|
\t\tif (!outcome) outcome = { status: "unknown" };`;
|
|
|
|
function main() {
|
|
const pkgRoot = resolveOpenClawPackageRoot();
|
|
const targets = [
|
|
path.join(pkgRoot, 'dist', 'reply-DeXK9BLT.js'),
|
|
path.join(pkgRoot, 'dist', 'compact-D3emcZgv.js'),
|
|
path.join(pkgRoot, 'dist', 'pi-embedded-CrsFdYam.js'),
|
|
path.join(pkgRoot, 'dist', 'pi-embedded-jHMb7qEG.js'),
|
|
path.join(pkgRoot, 'dist', 'plugin-sdk', 'dispatch-CJdFmoH9.js'),
|
|
].filter((file) => fs.existsSync(file));
|
|
|
|
const backupRoot = path.join(os.homedir(), '.openclaw', 'workspace', 'tmp', 'openclaw-subagent-outcome-hotfix');
|
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
const thisBackupDir = path.join(backupRoot, stamp);
|
|
let touched = 0;
|
|
|
|
for (const file of targets) {
|
|
let content = fs.readFileSync(file, 'utf8');
|
|
let changed = false;
|
|
|
|
for (const [label, oldText, newText] of [
|
|
['extractAssistantTerminalText', extractAssistantTextOld, extractAssistantTextNew],
|
|
['readLatestAssistantOutcome', readLatestAssistantReplyOld, readLatestAssistantReplyNew],
|
|
['wait outcome downgrade', waitOutcomeOld, waitOutcomeNew],
|
|
['announce error guard', announceGuardOld, announceGuardNew],
|
|
]) {
|
|
const result = replaceOnce(content, oldText, newText, label, file);
|
|
content = result.content;
|
|
changed = changed || result.changed;
|
|
}
|
|
|
|
if (changed) {
|
|
ensureDir(thisBackupDir);
|
|
const backupPath = path.join(thisBackupDir, path.basename(file));
|
|
fs.copyFileSync(file, backupPath);
|
|
fs.writeFileSync(file, content, 'utf8');
|
|
touched += 1;
|
|
console.log(`patched ${file}`);
|
|
} else {
|
|
console.log(`already patched ${file}`);
|
|
}
|
|
}
|
|
|
|
console.log(`done; touched ${touched} file(s)`);
|
|
if (touched > 0) console.log(`backup: ${thisBackupDir}`);
|
|
}
|
|
|
|
main();
|