From 5f1773a699ad78fd00c90c75d35b26c91ad7a9b8 Mon Sep 17 00:00:00 2001 From: Eve Date: Thu, 23 Apr 2026 00:21:01 +0800 Subject: [PATCH] Use file-based wrapper invocation in force-recall hook --- hooks/force-recall/handler.ts | 132 ---------------------------------- 1 file changed, 132 deletions(-) diff --git a/hooks/force-recall/handler.ts b/hooks/force-recall/handler.ts index 983bc20..e69de29 100644 --- a/hooks/force-recall/handler.ts +++ b/hooks/force-recall/handler.ts @@ -1,132 +0,0 @@ -import fs from "node:fs/promises"; -import path from "node:path"; -import { execFile } from "node:child_process"; -import { promisify } from "node:util"; - -const execFileAsync = promisify(execFile); - -function clamp(s: string, max = 1200): string { - if (!s) return s; - if (s.length <= max) return s; - return s.slice(0, max) + "\n…(truncated)…"; -} - -async function safeReadText(filePath: string): Promise { - try { - const raw = await fs.readFile(filePath, "utf-8"); - const trimmed = raw.trim(); - return trimmed ? trimmed : null; - } catch { - return null; - } -} - -async function runLongTaskWrapper(workspaceDir: string, ctx: any): Promise { - try { - const wrapperPath = path.join(workspaceDir, "scripts", "long_task_governor_wrapper.mjs"); - const input = { - requestText: (ctx.body ?? ctx.content ?? ctx.bodyForAgent ?? "") as string, - hasFilesOrSystems: false, - needsWaiting: false, - needsSubagent: false, - needsOwnerDecision: false, - canReplyNow: false, - taskName: "Hook preflight classification", - currentStep: "Classifying request at preprocessed hook", - nextStep: "Carry governor recommendation into prompt context", - nextReportCondition: "At next meaningful milestone", - waitingOn: "none", - blocker: "none", - checkpointTrigger: "", - externalizedTrigger: "", - triggerKind: "", - }; - - const { stdout } = await execFileAsync("node", [wrapperPath, "--compact", "--input", "-"], { - cwd: workspaceDir, - input: JSON.stringify(input), - maxBuffer: 1024 * 1024, - } as any); - - return JSON.parse(stdout); - } catch { - return null; - } -} - -/** - * Force Recall hook handler - * - * Event: message:preprocessed - * - Reads docs/RULEBOOK.md and SOUL.md from the resolved workspace - * - Prepends a recall gate block to context.bodyForAgent - * - Optionally injects wrapper MVP classification hints when available - */ -const forceRecall = async (event: any) => { - if (event?.type !== "message" || event?.action !== "preprocessed") return; - - const ctx = event.context ?? {}; - const workspaceDir: string | undefined = ctx.workspaceDir; - if (!workspaceDir) return; - - const rulebookPath = path.join(workspaceDir, "docs", "RULEBOOK.md"); - const soulPath = path.join(workspaceDir, "SOUL.md"); - - const [rulebook, soul, wrapperResult] = await Promise.all([ - safeReadText(rulebookPath), - safeReadText(soulPath), - runLongTaskWrapper(workspaceDir, ctx), - ]); - - if (!rulebook && !soul && !wrapperResult) return; - - const wrapperBlock = wrapperResult - ? [ - "[LONG_TASK_GOVERNOR_PREFLIGHT]", - `classification=${wrapperResult.classification}`, - `silentCandidate=${wrapperResult.silentCandidate}`, - `needsCheckpoint=${wrapperResult.needsCheckpoint}`, - `needsSubagent=${wrapperResult.needsSubagent}`, - `needsOwnerDecision=${wrapperResult.needsOwnerDecision}`, - `silentLaunchOk=${wrapperResult.silentLaunchOk}`, - wrapperResult.silentLaunchReason ? `silentLaunchReason=${wrapperResult.silentLaunchReason}` : null, - wrapperResult.recommendedFallback ? `recommendedFallback=${wrapperResult.recommendedFallback}` : null, - wrapperResult.requiredNextAction ? `requiredNextAction=${wrapperResult.requiredNextAction}` : null, - wrapperResult.handoff?.mode ? `handoff.mode=${wrapperResult.handoff.mode}` : null, - "- Treat this as preflight guidance from the wrapper MVP, not final truth.", - "- If classification=long_task, prefer task state + checkpoint discipline.", - "- If silentCandidate=true and silentLaunchOk=false, do not launch silent mode as-is.", - "[/LONG_TASK_GOVERNOR_PREFLIGHT]", - "", - ] - .filter(Boolean) - .join("\n") - : ""; - - const recallBlock = [ - "[RECALL_GATE] Mandatory recall before ANY technical action/tool use.", - "- You MUST consult and follow the key rules from RULEBOOK + SOUL.", - "- If you are about to run tools, change configs, modify code, or delegate agents: restate the applicable rules first.", - "", - wrapperBlock || null, - rulebook ? `RULEBOOK (source: ${rulebookPath}):\n${clamp(rulebook, 1200)}` : null, - soul ? `SOUL (source: ${soulPath}):\n${clamp(soul, 1200)}` : null, - "[/RECALL_GATE]", - "", - ] - .filter(Boolean) - .join("\n"); - - const prior = (ctx.bodyForAgent ?? ctx.body ?? ctx.content ?? "") as string; - const injected = `${recallBlock}${prior ? "\n" + prior : ""}`; - - ctx.bodyForAgent = injected; - event.context = ctx; - - if (process.env.OPENCLAW_FORCE_RECALL_DEBUG === "1") { - ctx.bodyForAgent += "\n\n[force-recall:debug] injected"; - console.log(`[force-recall:debug] injected for chat=${ctx.chatId ?? "?"} msg=${ctx.messageId ?? "?"}`); - } -}; - -export default forceRecall;