From 213804a6f777c48036b92c375241f8579676451a Mon Sep 17 00:00:00 2001 From: Eve Date: Fri, 24 Apr 2026 08:37:59 +0800 Subject: [PATCH] fix: emit real checkpoint artifact for wrapper integration --- hooks/force-recall/handler.ts | 40 ++++++++++++++++++++------ scripts/long_task_governor_wrapper.mjs | 38 ++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 10 deletions(-) diff --git a/hooks/force-recall/handler.ts b/hooks/force-recall/handler.ts index 979d393..13df26a 100644 --- a/hooks/force-recall/handler.ts +++ b/hooks/force-recall/handler.ts @@ -46,6 +46,23 @@ async function safeReadText(filePath: string): Promise { } } +async function getReadableCheckpointArtifact(workspaceDir: string, wrapperResult: any): Promise<{ relativePath: string; absolutePath: string; content: string; } | null> { + const relativePath = typeof wrapperResult?.externalizedCheckpointPath === "string" + ? wrapperResult.externalizedCheckpointPath.trim() + : ""; + if (!relativePath) return null; + + const absolutePath = path.resolve(workspaceDir, relativePath); + try { + const raw = await fs.readFile(absolutePath, "utf-8"); + const content = raw.trim(); + if (!content) return null; + return { relativePath, absolutePath, content }; + } catch { + return null; + } +} + async function runJsonScript(scriptPath: string, workspaceDir: string, input: Record, timeout: number): Promise { let tempInputPath: string | null = null; @@ -95,7 +112,7 @@ async function runLongTaskWrapper(workspaceDir: string, ctx: any): Promise | null { +function buildProgressEvidence(wrapperResult: any, readableCheckpointArtifact: { relativePath: string; absolutePath: string; content: string; } | null): Record | null { const candidate = wrapperResult?.progressEvidence; if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) { return null; @@ -128,6 +145,13 @@ function buildProgressEvidence(wrapperResult: any): Record | nu progressEvidence.verificationResult = verificationResult; } + if (readableCheckpointArtifact) { + progressEvidence.checkpointPath = readableCheckpointArtifact.relativePath; + if (!progressEvidence.verificationResult) { + progressEvidence.verificationResult = `checkpoint artifact readable at ${readableCheckpointArtifact.relativePath}`; + } + } + return Object.keys(progressEvidence).length > 0 ? progressEvidence : null; } @@ -158,14 +182,14 @@ function shouldClaimProgression(wrapperResult: any, progressEvidence: Record { +function buildGateLockInput(wrapperResult: any, readableCheckpointArtifact: { relativePath: string; absolutePath: string; content: string; } | null): Record { if (!wrapperResult || wrapperResult.classification !== "long_task") { return { classification: wrapperResult?.classification ?? "general_chat" }; } const needsOwnerDecision = wrapperResult.needsOwnerDecision === true; const silentCandidate = wrapperResult.silentCandidate === true; - const progressEvidence = buildProgressEvidence(wrapperResult); + const progressEvidence = buildProgressEvidence(wrapperResult, readableCheckpointArtifact); const requiredNextAction = typeof wrapperResult.requiredNextAction === "string" ? wrapperResult.requiredNextAction.trim() : ""; @@ -197,8 +221,7 @@ function buildGateLockInput(wrapperResult: any): Record { const progressEvidenceReason = claimedProgression && !progressEvidence ? "progression claim requires concrete evidence such as sessionKey, runId, modified_files, or verification result" : ""; - const hasExternalizedCheckpointEvidence = typeof wrapperResult.externalizedCheckpointPath === "string" - && wrapperResult.externalizedCheckpointPath.trim().length > 0; + const hasExternalizedCheckpointEvidence = Boolean(readableCheckpointArtifact); const hasButtonPathClosureEvidence = needsOwnerDecision && wrapperResult.silentLaunchOk === true; return { @@ -228,8 +251,8 @@ function buildGateLockInput(wrapperResult: any): Record { dispatchEvidence: "", fileChangeEvidence: "", verificationEvidence: "", - checkpointArtifactEvidence: hasExternalizedCheckpointEvidence ? wrapperResult.externalizedCheckpointPath.trim() : "", - externalizedCheckpointPath: hasExternalizedCheckpointEvidence ? wrapperResult.externalizedCheckpointPath.trim() : "", + checkpointArtifactEvidence: hasExternalizedCheckpointEvidence ? readableCheckpointArtifact.relativePath : "", + externalizedCheckpointPath: hasExternalizedCheckpointEvidence ? readableCheckpointArtifact.relativePath : "", externalizedTrigger: hasExternalizedCheckpointEvidence ? "hook-preflight-checkpoint" : "", handoffMode: hasButtonPathClosureEvidence ? (wrapperResult.handoff?.mode ?? "button_path") : "direct_reply", replyClosureMode: hasButtonPathClosureEvidence ? (wrapperResult.handoff?.mode ?? "button_path") : "direct_reply", @@ -238,7 +261,8 @@ function buildGateLockInput(wrapperResult: any): Record { async function runLongTaskGateLock(workspaceDir: string, wrapperResult: any): Promise { const gateLockPath = path.join(workspaceDir, "scripts", "long_task_gate_lock.mjs"); - const input = buildGateLockInput(wrapperResult); + const readableCheckpointArtifact = await getReadableCheckpointArtifact(workspaceDir, wrapperResult); + const input = buildGateLockInput(wrapperResult, readableCheckpointArtifact); return runJsonScript(gateLockPath, workspaceDir, input, LONG_TASK_GATE_LOCK_TIMEOUT_MS); } diff --git a/scripts/long_task_governor_wrapper.mjs b/scripts/long_task_governor_wrapper.mjs index 42430e1..ec6c79b 100644 --- a/scripts/long_task_governor_wrapper.mjs +++ b/scripts/long_task_governor_wrapper.mjs @@ -1,5 +1,6 @@ #!/usr/bin/env node import fs from 'fs'; +import path from 'path'; function fail(code, message) { process.stderr.write(`${code}: ${message}\n`); @@ -124,6 +125,35 @@ function toSlug(value) { .slice(0, 48); } +function ensureCheckpointArtifact(externalizedCheckpointPath, input, classificationResult) { + if (classificationResult.classification !== 'long_task') return null; + if (!classificationResult.silentCandidate) return null; + if (!externalizedCheckpointPath) return null; + + const artifactPath = path.resolve(process.cwd(), externalizedCheckpointPath); + const artifact = { + kind: 'long_task_checkpoint', + triggerKind: input.triggerKind || 'artifact', + checkpointTrigger: input.checkpointTrigger || '', + currentStep: input.currentStep || '', + nextStep: input.nextStep || '', + waitingOn: input.waitingOn || '', + blocker: input.blocker || '', + }; + + fs.mkdirSync(path.dirname(artifactPath), { recursive: true }); + fs.writeFileSync(artifactPath, JSON.stringify(artifact, null, 2) + '\n', 'utf8'); + + const stats = fs.statSync(artifactPath); + const readable = fs.readFileSync(artifactPath, 'utf8'); + + return { + absolutePath: artifactPath, + bytes: stats.size, + readable: readable.trim().length > 0, + }; +} + function buildExternalizedCheckpointPath(input, classificationResult) { if (classificationResult.classification !== 'long_task') return ''; if (!classificationResult.silentCandidate) return ''; @@ -138,14 +168,16 @@ function buildExternalizedCheckpointPath(input, classificationResult) { return `checkpoints/${stableSeed}.json`; } -function buildProgressEvidence(input, classificationResult, externalizedCheckpointPath) { +function buildProgressEvidence(input, classificationResult, externalizedCheckpointPath, checkpointArtifact) { if (classificationResult.classification !== 'long_task') return null; if (!classificationResult.silentCandidate) return null; if (!externalizedCheckpointPath) return null; + if (!checkpointArtifact || checkpointArtifact.readable !== true) return null; return { sessionKey: toSlug([input.currentStep, input.waitingOn, input.nextStep].filter(Boolean).join('-')) || 'long-task-session', checkpointPath: externalizedCheckpointPath, + verificationResult: `checkpoint artifact readable at ${externalizedCheckpointPath}`, }; } @@ -197,7 +229,8 @@ function main() { const classificationResult = classify(input); const taskRecord = bootstrapTaskState(input, classificationResult); const externalizedCheckpointPath = buildExternalizedCheckpointPath(input, classificationResult); - const progressEvidence = buildProgressEvidence(input, classificationResult, externalizedCheckpointPath); + const checkpointArtifact = ensureCheckpointArtifact(externalizedCheckpointPath, input, classificationResult); + const progressEvidence = buildProgressEvidence(input, classificationResult, externalizedCheckpointPath, checkpointArtifact); const silentLaunch = validateSilentLaunch(input, classificationResult); const handoff = planHandoff(classificationResult); @@ -210,6 +243,7 @@ function main() { taskRecord, progressEvidence, externalizedCheckpointPath, + checkpointArtifact, silentLaunchOk: silentLaunch.ok, silentLaunchReason: silentLaunch.reason, recommendedFallback: silentLaunch.recommendedFallback,