Files
swarm-zap/scripts/apply-openclaw-subagent-outcome-hotfix.js

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();