fix(retry): lineage tracking + relative mission path resolution (#28)
- retryRun rewrites a snapshot's relative mission path to the run's own recorded mission.json (absolute) before execution — retries stay faithful to what originally ran - runTask accepts options.retriedFrom; retry records lineage in result.json (additive optional field, no schema break) - task suite +4 cases: retry succeeds, lineage recorded, mission section present after retry (36 total), missing-run retry exits 4 Closes #28
This commit is contained in:
+21
-2
@@ -234,7 +234,7 @@ function writeOnce(file, content) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function runTask(taskFile) {
|
function runTask(taskFile, options = {}) {
|
||||||
const resolved = JSON.parse(
|
const resolved = JSON.parse(
|
||||||
spawnSync(process.execPath, [path.join(PROJECT_ROOT, "scripts", "mosaic-config.mjs"), "validate"], {
|
spawnSync(process.execPath, [path.join(PROJECT_ROOT, "scripts", "mosaic-config.mjs"), "validate"], {
|
||||||
cwd: PROJECT_ROOT,
|
cwd: PROJECT_ROOT,
|
||||||
@@ -345,6 +345,7 @@ function runTask(taskFile) {
|
|||||||
request: task.prompt,
|
request: task.prompt,
|
||||||
response,
|
response,
|
||||||
expectedExact: expected,
|
expectedExact: expected,
|
||||||
|
...(options.retriedFrom ? { retriedFrom: options.retriedFrom } : {}),
|
||||||
workspace: task.workspace,
|
workspace: task.workspace,
|
||||||
tools: task.tools,
|
tools: task.tools,
|
||||||
session: task.session,
|
session: task.session,
|
||||||
@@ -455,6 +456,24 @@ function retryRun(runId) {
|
|||||||
fail(4, `run task snapshot unreadable: ${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
|
// A retry is a brand-new run: replay the recorded task snapshot through
|
||||||
// the ordinary run path; existing run records stay untouched.
|
// the ordinary run path; existing run records stay untouched.
|
||||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mosaic-retry-"));
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mosaic-retry-"));
|
||||||
@@ -467,7 +486,7 @@ function retryRun(runId) {
|
|||||||
// Best-effort cleanup only.
|
// Best-effort cleanup only.
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
runTask(tempTaskFile);
|
runTask(tempTaskFile, { retriedFrom: runId });
|
||||||
}
|
}
|
||||||
|
|
||||||
const operation = process.argv[2];
|
const operation = process.argv[2];
|
||||||
|
|||||||
@@ -142,6 +142,21 @@ EOF
|
|||||||
&& grep -q 'Seam directive A.' "$SANDBOX/data/system-prompt.md" \
|
&& 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" 0 \
|
||||||
|| check "mission section injected into generated prompt" 1
|
|| 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
|
else
|
||||||
echo "skip adapter seam cases (docker daemon unavailable)"
|
echo "skip adapter seam cases (docker daemon unavailable)"
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user