From 6341bd9fb01654f31b8884a37b3baeaaf6202634 Mon Sep 17 00:00:00 2001 From: zap Date: Tue, 17 Mar 2026 01:01:10 +0000 Subject: [PATCH] chore(scripts): add openclaw subagent outcome hotfix script --- .../apply-openclaw-subagent-outcome-hotfix.js | 231 ++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100755 scripts/apply-openclaw-subagent-outcome-hotfix.js diff --git a/scripts/apply-openclaw-subagent-outcome-hotfix.js b/scripts/apply-openclaw-subagent-outcome-hotfix.js new file mode 100755 index 0000000..fb4c6b1 --- /dev/null +++ b/scripts/apply-openclaw-subagent-outcome-hotfix.js @@ -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();