chore(scripts): add openclaw subagent outcome hotfix script
This commit is contained in:
231
scripts/apply-openclaw-subagent-outcome-hotfix.js
Executable file
231
scripts/apply-openclaw-subagent-outcome-hotfix.js
Executable file
@@ -0,0 +1,231 @@
|
||||
#!/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();
|
||||
Reference in New Issue
Block a user