diff --git a/scripts/mosaic-task.mjs b/scripts/mosaic-task.mjs index e59c1a2c..8eb96ce9 100755 --- a/scripts/mosaic-task.mjs +++ b/scripts/mosaic-task.mjs @@ -234,7 +234,7 @@ function writeOnce(file, content) { } } -function runTask(taskFile) { +function runTask(taskFile, options = {}) { const resolved = JSON.parse( spawnSync(process.execPath, [path.join(PROJECT_ROOT, "scripts", "mosaic-config.mjs"), "validate"], { cwd: PROJECT_ROOT, @@ -345,6 +345,7 @@ function runTask(taskFile) { request: task.prompt, response, expectedExact: expected, + ...(options.retriedFrom ? { retriedFrom: options.retriedFrom } : {}), workspace: task.workspace, tools: task.tools, session: task.session, @@ -455,6 +456,24 @@ function retryRun(runId) { fail(4, `run task snapshot unreadable: ${runId}`); } + // Relative mission paths in a snapshot resolve against the ORIGINAL task + // location, which no longer exists here — rewrite them to the run's own + // recorded mission.json so retries stay faithful. + let snapshotDoc; + try { + snapshotDoc = JSON.parse(snapshot); + } catch { + fail(4, `run task snapshot is not valid JSON: ${runId}`); + } + if (snapshotDoc.mission && !path.isAbsolute(snapshotDoc.mission)) { + const recordedMission = path.join(dir, "mission.json"); + if (!fs.existsSync(recordedMission)) { + fail(4, `cannot retry ${runId}: relative mission path but no mission.json snapshot in run dir`); + } + snapshotDoc.mission = recordedMission; + snapshot = `${JSON.stringify(snapshotDoc, null, 2)}\n`; + } + // A retry is a brand-new run: replay the recorded task snapshot through // the ordinary run path; existing run records stay untouched. const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mosaic-retry-")); @@ -467,7 +486,7 @@ function retryRun(runId) { // Best-effort cleanup only. } }); - runTask(tempTaskFile); + runTask(tempTaskFile, { retriedFrom: runId }); } const operation = process.argv[2]; diff --git a/scripts/test-task.sh b/scripts/test-task.sh index b4333391..d50a0302 100755 --- a/scripts/test-task.sh +++ b/scripts/test-task.sh @@ -142,6 +142,21 @@ EOF && grep -q 'Seam directive A.' "$SANDBOX/data/system-prompt.md" \ && check "mission section injected into generated prompt" 0 \ || check "mission section injected into generated prompt" 1 + + # retry lineage + relative mission path resolution + expect_exit "retry of mission run succeeds" 0 -- \ + env MOSAIC_CONFIG="$SANDBOX/mock-adapters.json" MOSAIC_MOCK_RESPONSE=MOCKED \ + node scripts/mosaic-task.mjs retry "$(ls -dt "$SANDBOX/data/runs"/r-* | head -1 | xargs basename)" + RT="$(ls -dt "$SANDBOX/data/runs"/r-* | head -1 | xargs basename)" + ORIG="$(ls -dt "$SANDBOX/data/runs"/r-* | sed -n 2p | xargs basename)" + node -e 'const r=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));process.exit(r.retriedFrom===process.argv[2]?0:1)' \ + "$SANDBOX/data/runs/$RT/result.json" "$ORIG" \ + && check "retriedFrom lineage recorded" 0 || check "retriedFrom lineage recorded" 1 + grep -q 'MISSION (runtime)' "$SANDBOX/data/system-prompt.md" \ + && check "mission section present after retry (relative path resolved)" 0 \ + || check "mission section present after retry (relative path resolved)" 1 + expect_exit "retry of missing run exits 4" 4 -- \ + env MOSAIC_CONFIG="$SANDBOX/mock-adapters.json" node scripts/mosaic-task.mjs retry r-missing else echo "skip adapter seam cases (docker daemon unavailable)" fi