@a5c-ai/babysitter-sdk 0.0.40 → 0.0.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +114 -0
- package/dist/runtime/commitEffectResult.d.ts.map +1 -1
- package/dist/runtime/commitEffectResult.js +55 -52
- package/dist/runtime/orchestrateIteration.d.ts.map +1 -1
- package/dist/runtime/orchestrateIteration.js +93 -90
- package/dist/storage/lock.d.ts +5 -0
- package/dist/storage/lock.d.ts.map +1 -1
- package/dist/storage/lock.js +35 -0
- package/package.json +1 -1
package/dist/cli/main.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":";AA43CA,wBAAgB,mBAAmB;eAEf,MAAM,EAAE,GAA2B,OAAO,CAAC,MAAM,CAAC;kBAyCpD,MAAM;EAIvB"}
|
package/dist/cli/main.js
CHANGED
|
@@ -37,10 +37,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
37
37
|
exports.createBabysitterCli = createBabysitterCli;
|
|
38
38
|
const node_fs_1 = require("node:fs");
|
|
39
39
|
const path = __importStar(require("node:path"));
|
|
40
|
+
const crypto = __importStar(require("node:crypto"));
|
|
40
41
|
const commitEffectResult_1 = require("../runtime/commitEffectResult");
|
|
41
42
|
const createRun_1 = require("../runtime/createRun");
|
|
42
43
|
const effectIndex_1 = require("../runtime/replay/effectIndex");
|
|
43
44
|
const stateCache_1 = require("../runtime/replay/stateCache");
|
|
45
|
+
const ulids_1 = require("../storage/ulids");
|
|
44
46
|
const tasks_1 = require("../storage/tasks");
|
|
45
47
|
const journal_1 = require("../storage/journal");
|
|
46
48
|
const runFiles_1 = require("../storage/runFiles");
|
|
@@ -50,6 +52,7 @@ const USAGE = `Usage:
|
|
|
50
52
|
babysitter run:status <runDir> [--runs-dir <dir>] [--json]
|
|
51
53
|
babysitter run:events <runDir> [--runs-dir <dir>] [--json] [--limit <n>] [--reverse] [--filter-type <type>]
|
|
52
54
|
babysitter run:rebuild-state <runDir> [--runs-dir <dir>] [--json] [--dry-run]
|
|
55
|
+
babysitter run:repair-journal <runDir> [--runs-dir <dir>] [--json] [--dry-run]
|
|
53
56
|
babysitter run:iterate <runDir> [--runs-dir <dir>] [--json] [--verbose] [--iteration <n>]
|
|
54
57
|
babysitter task:post <runDir> <effectId> --status <ok|error> [--runs-dir <dir>] [--json] [--dry-run] [--value <file>] [--error <file>] [--stdout-ref <ref>] [--stderr-ref <ref>] [--stdout-file <file>] [--stderr-file <file>] [--started-at <iso8601>] [--finished-at <iso8601>] [--metadata <file>] [--invocation-key <key>]
|
|
55
58
|
babysitter task:list <runDir> [--runs-dir <dir>] [--pending] [--kind <kind>] [--json]
|
|
@@ -223,6 +226,9 @@ function parseArgs(argv) {
|
|
|
223
226
|
else if (parsed.command === "run:rebuild-state") {
|
|
224
227
|
[parsed.runDirArg] = positionals;
|
|
225
228
|
}
|
|
229
|
+
else if (parsed.command === "run:repair-journal") {
|
|
230
|
+
[parsed.runDirArg] = positionals;
|
|
231
|
+
}
|
|
226
232
|
return parsed;
|
|
227
233
|
}
|
|
228
234
|
function resolveRunDir(baseDir, runDirArg) {
|
|
@@ -689,6 +695,111 @@ async function handleRunRebuildState(parsed) {
|
|
|
689
695
|
console.log(`[run:rebuild-state] runDir=${runDir}${suffix}`);
|
|
690
696
|
return 0;
|
|
691
697
|
}
|
|
698
|
+
async function handleRunRepairJournal(parsed) {
|
|
699
|
+
if (!parsed.runDirArg) {
|
|
700
|
+
console.error(USAGE);
|
|
701
|
+
return 1;
|
|
702
|
+
}
|
|
703
|
+
const runDir = resolveRunDir(parsed.runsDir, parsed.runDirArg);
|
|
704
|
+
logVerbose("run:repair-journal", parsed, {
|
|
705
|
+
runDir,
|
|
706
|
+
dryRun: parsed.dryRun,
|
|
707
|
+
json: parsed.json,
|
|
708
|
+
});
|
|
709
|
+
if (!(await readRunMetadataSafe(runDir, "run:repair-journal")))
|
|
710
|
+
return 1;
|
|
711
|
+
const journalDir = path.join(runDir, "journal");
|
|
712
|
+
const files = (await node_fs_1.promises.readdir(journalDir)).filter((name) => name.endsWith(".json")).sort();
|
|
713
|
+
const rawEvents = [];
|
|
714
|
+
for (const filename of files) {
|
|
715
|
+
const fullPath = path.join(journalDir, filename);
|
|
716
|
+
const payload = JSON.parse(await node_fs_1.promises.readFile(fullPath, "utf8"));
|
|
717
|
+
rawEvents.push({ filename, payload });
|
|
718
|
+
}
|
|
719
|
+
const seenInvocation = new Set();
|
|
720
|
+
const keptEffectIds = new Set();
|
|
721
|
+
const droppedEffectIds = new Set();
|
|
722
|
+
const kept = [];
|
|
723
|
+
let droppedRequested = 0;
|
|
724
|
+
let droppedResolved = 0;
|
|
725
|
+
for (const entry of rawEvents) {
|
|
726
|
+
const type = typeof entry.payload.type === "string" ? entry.payload.type : "UNKNOWN";
|
|
727
|
+
const recordedAt = typeof entry.payload.recordedAt === "string" ? entry.payload.recordedAt : undefined;
|
|
728
|
+
const data = isJsonRecord(entry.payload.data) ? entry.payload.data : {};
|
|
729
|
+
if (type === "EFFECT_REQUESTED") {
|
|
730
|
+
const invocationKey = typeof data.invocationKey === "string" ? data.invocationKey : "";
|
|
731
|
+
const effectId = typeof data.effectId === "string" ? data.effectId : "";
|
|
732
|
+
if (invocationKey && seenInvocation.has(invocationKey)) {
|
|
733
|
+
droppedRequested += 1;
|
|
734
|
+
if (effectId)
|
|
735
|
+
droppedEffectIds.add(effectId);
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
if (invocationKey)
|
|
739
|
+
seenInvocation.add(invocationKey);
|
|
740
|
+
if (effectId)
|
|
741
|
+
keptEffectIds.add(effectId);
|
|
742
|
+
kept.push({ type, recordedAt, data });
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
if (type === "EFFECT_RESOLVED") {
|
|
746
|
+
const effectId = typeof data.effectId === "string" ? data.effectId : "";
|
|
747
|
+
if (effectId && droppedEffectIds.has(effectId) && !keptEffectIds.has(effectId)) {
|
|
748
|
+
droppedResolved += 1;
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
kept.push({ type, recordedAt, data });
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
// Keep all other events.
|
|
755
|
+
kept.push({ type, recordedAt, data });
|
|
756
|
+
}
|
|
757
|
+
const summary = {
|
|
758
|
+
runDir,
|
|
759
|
+
journal: {
|
|
760
|
+
originalFiles: files.length,
|
|
761
|
+
keptEvents: kept.length,
|
|
762
|
+
droppedRequested,
|
|
763
|
+
droppedResolved,
|
|
764
|
+
},
|
|
765
|
+
};
|
|
766
|
+
if (parsed.dryRun) {
|
|
767
|
+
if (parsed.json) {
|
|
768
|
+
console.log(JSON.stringify({ dryRun: true, ...summary }));
|
|
769
|
+
}
|
|
770
|
+
else {
|
|
771
|
+
console.log(`[run:repair-journal] dry-run originalFiles=${files.length} keptEvents=${kept.length} droppedRequested=${droppedRequested} droppedResolved=${droppedResolved}`);
|
|
772
|
+
}
|
|
773
|
+
return 0;
|
|
774
|
+
}
|
|
775
|
+
const stamp = Date.now();
|
|
776
|
+
const repairedDir = path.join(runDir, `journal.repaired.${stamp}`);
|
|
777
|
+
await node_fs_1.promises.mkdir(repairedDir, { recursive: true });
|
|
778
|
+
for (let i = 0; i < kept.length; i += 1) {
|
|
779
|
+
const seq = String(i + 1).padStart(6, "0");
|
|
780
|
+
const ulid = (0, ulids_1.nextUlid)();
|
|
781
|
+
const filename = `${seq}.${ulid}.json`;
|
|
782
|
+
const eventPayload = {
|
|
783
|
+
type: kept[i].type,
|
|
784
|
+
recordedAt: kept[i].recordedAt ?? new Date().toISOString(),
|
|
785
|
+
data: kept[i].data,
|
|
786
|
+
};
|
|
787
|
+
const contents = JSON.stringify(eventPayload, null, 2) + "\n";
|
|
788
|
+
const checksum = crypto.createHash("sha256").update(contents).digest("hex");
|
|
789
|
+
const withChecksum = JSON.stringify({ ...eventPayload, checksum }, null, 2) + "\n";
|
|
790
|
+
await node_fs_1.promises.writeFile(path.join(repairedDir, filename), withChecksum, "utf8");
|
|
791
|
+
}
|
|
792
|
+
const backupDir = path.join(runDir, `journal.bak.${stamp}`);
|
|
793
|
+
await node_fs_1.promises.rename(journalDir, backupDir);
|
|
794
|
+
await node_fs_1.promises.rename(repairedDir, journalDir);
|
|
795
|
+
if (parsed.json) {
|
|
796
|
+
console.log(JSON.stringify({ ...summary, backupDir, repaired: true }));
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
console.log(`[run:repair-journal] repaired originalFiles=${files.length} keptEvents=${kept.length} droppedRequested=${droppedRequested} droppedResolved=${droppedResolved} backupDir=${backupDir}`);
|
|
800
|
+
}
|
|
801
|
+
return 0;
|
|
802
|
+
}
|
|
692
803
|
async function handleTaskPost(parsed) {
|
|
693
804
|
if (!parsed.runDirArg || !parsed.effectId) {
|
|
694
805
|
console.error(USAGE);
|
|
@@ -1190,6 +1301,9 @@ function createBabysitterCli() {
|
|
|
1190
1301
|
if (parsed.command === "run:rebuild-state") {
|
|
1191
1302
|
return await handleRunRebuildState(parsed);
|
|
1192
1303
|
}
|
|
1304
|
+
if (parsed.command === "run:repair-journal") {
|
|
1305
|
+
return await handleRunRepairJournal(parsed);
|
|
1306
|
+
}
|
|
1193
1307
|
if (parsed.command === "run:status") {
|
|
1194
1308
|
return await handleRunStatus(parsed);
|
|
1195
1309
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commitEffectResult.d.ts","sourceRoot":"","sources":["../../src/runtime/commitEffectResult.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"commitEffectResult.d.ts","sourceRoot":"","sources":["../../src/runtime/commitEffectResult.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,2BAA2B,EAC3B,yBAAyB,EAG1B,MAAM,SAAS,CAAC;AAOjB,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,2BAA2B,CAAC,CAsEjH"}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.commitEffectResult = commitEffectResult;
|
|
4
4
|
const journal_1 = require("../storage/journal");
|
|
5
|
+
const lock_1 = require("../storage/lock");
|
|
5
6
|
const effectIndex_1 = require("./replay/effectIndex");
|
|
6
7
|
const exceptions_1 = require("./exceptions");
|
|
7
8
|
const errorUtils_1 = require("./errorUtils");
|
|
@@ -9,65 +10,67 @@ const instrumentation_1 = require("./instrumentation");
|
|
|
9
10
|
const registry_1 = require("../tasks/registry");
|
|
10
11
|
const serializer_1 = require("../tasks/serializer");
|
|
11
12
|
async function commitEffectResult(options) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
taskId: requireTaskId(record),
|
|
29
|
-
invocationKey: record.invocationKey,
|
|
30
|
-
payload: resultPayload,
|
|
31
|
-
});
|
|
32
|
-
const stdoutRef = resultPayload.stdoutRef ?? writtenStdoutRef;
|
|
33
|
-
const stderrRef = resultPayload.stderrRef ?? writtenStderrRef;
|
|
34
|
-
const eventError = resultPayload.status === "error" ? resultPayload.error : undefined;
|
|
35
|
-
const resolvedEvent = await (0, journal_1.appendEvent)({
|
|
36
|
-
runDir: options.runDir,
|
|
37
|
-
eventType: "EFFECT_RESOLVED",
|
|
38
|
-
event: {
|
|
13
|
+
return await (0, lock_1.withRunLock)(options.runDir, "runtime:commitEffectResult", async () => {
|
|
14
|
+
guardResultPayload(options);
|
|
15
|
+
const effectIndex = await (0, effectIndex_1.buildEffectIndex)({ runDir: options.runDir });
|
|
16
|
+
const record = effectIndex.getByEffectId(options.effectId);
|
|
17
|
+
if (!record) {
|
|
18
|
+
logCommitFailure(options, "unknown_effect");
|
|
19
|
+
throw new exceptions_1.RunFailedError(`Unknown effectId ${options.effectId}`);
|
|
20
|
+
}
|
|
21
|
+
if (record.status !== "requested") {
|
|
22
|
+
logCommitFailure(options, "already_resolved", { currentStatus: record.status });
|
|
23
|
+
throw new exceptions_1.RunFailedError(`Effect ${options.effectId} is already resolved`);
|
|
24
|
+
}
|
|
25
|
+
ensureInvocationKeyMatches(options, record);
|
|
26
|
+
const resultPayload = buildResultPayload(options);
|
|
27
|
+
const { resultRef, stdoutRef: writtenStdoutRef, stderrRef: writtenStderrRef } = await (0, serializer_1.serializeAndWriteTaskResult)({
|
|
28
|
+
runDir: options.runDir,
|
|
39
29
|
effectId: options.effectId,
|
|
40
|
-
|
|
30
|
+
taskId: requireTaskId(record),
|
|
31
|
+
invocationKey: record.invocationKey,
|
|
32
|
+
payload: resultPayload,
|
|
33
|
+
});
|
|
34
|
+
const stdoutRef = resultPayload.stdoutRef ?? writtenStdoutRef;
|
|
35
|
+
const stderrRef = resultPayload.stderrRef ?? writtenStderrRef;
|
|
36
|
+
const eventError = resultPayload.status === "error" ? resultPayload.error : undefined;
|
|
37
|
+
const resolvedEvent = await (0, journal_1.appendEvent)({
|
|
38
|
+
runDir: options.runDir,
|
|
39
|
+
eventType: "EFFECT_RESOLVED",
|
|
40
|
+
event: {
|
|
41
|
+
effectId: options.effectId,
|
|
42
|
+
status: options.result.status,
|
|
43
|
+
resultRef,
|
|
44
|
+
error: eventError,
|
|
45
|
+
stdoutRef,
|
|
46
|
+
stderrRef,
|
|
47
|
+
startedAt: resultPayload.startedAt,
|
|
48
|
+
finishedAt: resultPayload.finishedAt,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
registry_1.globalTaskRegistry.resolveEffect(options.effectId, {
|
|
52
|
+
status: options.result.status === "ok" ? "resolved_ok" : "resolved_error",
|
|
41
53
|
resultRef,
|
|
42
|
-
error: eventError,
|
|
43
54
|
stdoutRef,
|
|
44
55
|
stderrRef,
|
|
56
|
+
resolvedAt: resolvedEvent.recordedAt,
|
|
57
|
+
});
|
|
58
|
+
(0, instrumentation_1.emitRuntimeMetric)(options.logger, "commit.effect", {
|
|
59
|
+
effectId: options.effectId,
|
|
60
|
+
invocationKey: record.invocationKey,
|
|
61
|
+
status: options.result.status,
|
|
62
|
+
runDir: options.runDir,
|
|
63
|
+
hasStdout: Boolean(stdoutRef),
|
|
64
|
+
hasStderr: Boolean(stderrRef),
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
resultRef,
|
|
68
|
+
stdoutRef: stdoutRef ?? undefined,
|
|
69
|
+
stderrRef: stderrRef ?? undefined,
|
|
45
70
|
startedAt: resultPayload.startedAt,
|
|
46
71
|
finishedAt: resultPayload.finishedAt,
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
registry_1.globalTaskRegistry.resolveEffect(options.effectId, {
|
|
50
|
-
status: options.result.status === "ok" ? "resolved_ok" : "resolved_error",
|
|
51
|
-
resultRef,
|
|
52
|
-
stdoutRef,
|
|
53
|
-
stderrRef,
|
|
54
|
-
resolvedAt: resolvedEvent.recordedAt,
|
|
55
|
-
});
|
|
56
|
-
(0, instrumentation_1.emitRuntimeMetric)(options.logger, "commit.effect", {
|
|
57
|
-
effectId: options.effectId,
|
|
58
|
-
invocationKey: record.invocationKey,
|
|
59
|
-
status: options.result.status,
|
|
60
|
-
runDir: options.runDir,
|
|
61
|
-
hasStdout: Boolean(stdoutRef),
|
|
62
|
-
hasStderr: Boolean(stderrRef),
|
|
72
|
+
};
|
|
63
73
|
});
|
|
64
|
-
return {
|
|
65
|
-
resultRef,
|
|
66
|
-
stdoutRef: stdoutRef ?? undefined,
|
|
67
|
-
stderrRef: stderrRef ?? undefined,
|
|
68
|
-
startedAt: resultPayload.startedAt,
|
|
69
|
-
finishedAt: resultPayload.finishedAt,
|
|
70
|
-
};
|
|
71
74
|
}
|
|
72
75
|
function ensureInvocationKeyMatches(options, record) {
|
|
73
76
|
if (!options.invocationKey)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"orchestrateIteration.d.ts","sourceRoot":"","sources":["../../src/runtime/orchestrateIteration.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"orchestrateIteration.d.ts","sourceRoot":"","sources":["../../src/runtime/orchestrateIteration.ts"],"names":[],"mappings":"AAaA,OAAO,EAEL,eAAe,EACf,kBAAkB,EAGnB,MAAM,SAAS,CAAC;AAYjB,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,eAAe,CAAC,CA6HhG"}
|
|
@@ -8,6 +8,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
8
8
|
const url_1 = require("url");
|
|
9
9
|
const journal_1 = require("../storage/journal");
|
|
10
10
|
const runFiles_1 = require("../storage/runFiles");
|
|
11
|
+
const lock_1 = require("../storage/lock");
|
|
11
12
|
const createReplayEngine_1 = require("./replay/createReplayEngine");
|
|
12
13
|
const processContext_1 = require("./processContext");
|
|
13
14
|
const exceptions_1 = require("./exceptions");
|
|
@@ -17,104 +18,106 @@ const runtime_1 = require("./hooks/runtime");
|
|
|
17
18
|
// Use an indirect dynamic import so TypeScript does not downlevel to require() in CommonJS builds.
|
|
18
19
|
const dynamicImportModule = new Function("specifier", "return import(specifier);");
|
|
19
20
|
async function orchestrateIteration(options) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
iteration: engine.replayCursor.value,
|
|
39
|
-
}, {
|
|
40
|
-
cwd: projectRoot,
|
|
41
|
-
logger,
|
|
42
|
-
});
|
|
43
|
-
try {
|
|
44
|
-
const output = await (0, processContext_1.withProcessContext)(engine.internalContext, () => processFn(inputs, engine.context, options.context));
|
|
45
|
-
const outputRef = await (0, runFiles_1.writeRunOutput)(options.runDir, output);
|
|
46
|
-
await (0, journal_1.appendEvent)({
|
|
47
|
-
runDir: options.runDir,
|
|
48
|
-
eventType: "RUN_COMPLETED",
|
|
49
|
-
event: {
|
|
50
|
-
outputRef,
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
// Call on-run-complete hook
|
|
54
|
-
await (0, runtime_1.callRuntimeHook)("on-run-complete", {
|
|
21
|
+
return await (0, lock_1.withRunLock)(options.runDir, "runtime:orchestrateIteration", async () => {
|
|
22
|
+
const iterationStartedAt = Date.now();
|
|
23
|
+
const nowFn = resolveNow(options.now);
|
|
24
|
+
const engine = await initializeReplayEngine(options, nowFn, iterationStartedAt);
|
|
25
|
+
const defaultEntrypoint = {
|
|
26
|
+
importPath: engine.metadata.entrypoint?.importPath ?? engine.metadata.processPath,
|
|
27
|
+
exportName: engine.metadata.entrypoint?.exportName,
|
|
28
|
+
};
|
|
29
|
+
const processFn = await loadProcessFunction(options, defaultEntrypoint, options.runDir);
|
|
30
|
+
const inputs = options.inputs ?? engine.inputs;
|
|
31
|
+
let finalStatus = "failed";
|
|
32
|
+
const logger = engine.internalContext.logger ?? options.logger;
|
|
33
|
+
// Compute project root for hook calls (parent of .a5c dir where plugins/ is located)
|
|
34
|
+
// runDir is like: /path/to/project/.a5c/runs/<runId>
|
|
35
|
+
// So we need 3 levels up: runs -> .a5c -> project
|
|
36
|
+
const projectRoot = path_1.default.dirname(path_1.default.dirname(path_1.default.dirname(options.runDir)));
|
|
37
|
+
// Call on-iteration-start hook
|
|
38
|
+
await (0, runtime_1.callRuntimeHook)("on-iteration-start", {
|
|
55
39
|
runId: engine.runId,
|
|
56
|
-
|
|
57
|
-
output,
|
|
58
|
-
duration: Date.now() - iterationStartedAt,
|
|
40
|
+
iteration: engine.replayCursor.value,
|
|
59
41
|
}, {
|
|
60
42
|
cwd: projectRoot,
|
|
61
43
|
logger,
|
|
62
44
|
});
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
45
|
+
try {
|
|
46
|
+
const output = await (0, processContext_1.withProcessContext)(engine.internalContext, () => processFn(inputs, engine.context, options.context));
|
|
47
|
+
const outputRef = await (0, runFiles_1.writeRunOutput)(options.runDir, output);
|
|
48
|
+
await (0, journal_1.appendEvent)({
|
|
49
|
+
runDir: options.runDir,
|
|
50
|
+
eventType: "RUN_COMPLETED",
|
|
51
|
+
event: {
|
|
52
|
+
outputRef,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
// Call on-run-complete hook
|
|
56
|
+
await (0, runtime_1.callRuntimeHook)("on-run-complete", {
|
|
57
|
+
runId: engine.runId,
|
|
58
|
+
status: "completed",
|
|
59
|
+
output,
|
|
60
|
+
duration: Date.now() - iterationStartedAt,
|
|
61
|
+
}, {
|
|
62
|
+
cwd: projectRoot,
|
|
63
|
+
logger,
|
|
64
|
+
});
|
|
65
|
+
const result = { status: "completed", output, metadata: createIterationMetadata(engine) };
|
|
66
|
+
finalStatus = result.status;
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
const waiting = asWaitingResult(error);
|
|
71
|
+
if (waiting) {
|
|
72
|
+
finalStatus = waiting.status;
|
|
73
|
+
return {
|
|
74
|
+
status: "waiting",
|
|
75
|
+
nextActions: annotateWaitingActions(waiting.nextActions),
|
|
76
|
+
metadata: createIterationMetadata(engine),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const failure = (0, errorUtils_1.serializeUnknownError)(error);
|
|
80
|
+
await (0, journal_1.appendEvent)({
|
|
81
|
+
runDir: options.runDir,
|
|
82
|
+
eventType: "RUN_FAILED",
|
|
83
|
+
event: { error: failure },
|
|
84
|
+
});
|
|
85
|
+
// Call on-run-fail hook
|
|
86
|
+
await (0, runtime_1.callRuntimeHook)("on-run-fail", {
|
|
87
|
+
runId: engine.runId,
|
|
88
|
+
status: "failed",
|
|
89
|
+
error: failure.message || "Unknown error",
|
|
90
|
+
duration: Date.now() - iterationStartedAt,
|
|
91
|
+
}, {
|
|
92
|
+
cwd: projectRoot,
|
|
93
|
+
logger,
|
|
94
|
+
});
|
|
95
|
+
const result = {
|
|
96
|
+
status: "failed",
|
|
97
|
+
error: failure,
|
|
74
98
|
metadata: createIterationMetadata(engine),
|
|
75
99
|
};
|
|
100
|
+
finalStatus = result.status;
|
|
101
|
+
return result;
|
|
76
102
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
error: failure,
|
|
96
|
-
metadata: createIterationMetadata(engine),
|
|
97
|
-
};
|
|
98
|
-
finalStatus = result.status;
|
|
99
|
-
return result;
|
|
100
|
-
}
|
|
101
|
-
finally {
|
|
102
|
-
(0, instrumentation_1.emitRuntimeMetric)(logger, "replay.iteration", {
|
|
103
|
-
duration_ms: Date.now() - iterationStartedAt,
|
|
104
|
-
status: finalStatus,
|
|
105
|
-
runId: engine.runId,
|
|
106
|
-
stepCount: engine.replayCursor.value,
|
|
107
|
-
});
|
|
108
|
-
// Call on-iteration-end hook
|
|
109
|
-
await (0, runtime_1.callRuntimeHook)("on-iteration-end", {
|
|
110
|
-
runId: engine.runId,
|
|
111
|
-
iteration: engine.replayCursor.value,
|
|
112
|
-
status: finalStatus,
|
|
113
|
-
}, {
|
|
114
|
-
cwd: projectRoot,
|
|
115
|
-
logger,
|
|
116
|
-
});
|
|
117
|
-
}
|
|
103
|
+
finally {
|
|
104
|
+
(0, instrumentation_1.emitRuntimeMetric)(logger, "replay.iteration", {
|
|
105
|
+
duration_ms: Date.now() - iterationStartedAt,
|
|
106
|
+
status: finalStatus,
|
|
107
|
+
runId: engine.runId,
|
|
108
|
+
stepCount: engine.replayCursor.value,
|
|
109
|
+
});
|
|
110
|
+
// Call on-iteration-end hook
|
|
111
|
+
await (0, runtime_1.callRuntimeHook)("on-iteration-end", {
|
|
112
|
+
runId: engine.runId,
|
|
113
|
+
iteration: engine.replayCursor.value,
|
|
114
|
+
status: finalStatus,
|
|
115
|
+
}, {
|
|
116
|
+
cwd: projectRoot,
|
|
117
|
+
logger,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|
|
118
121
|
}
|
|
119
122
|
async function loadProcessFunction(options, defaults, runDir) {
|
|
120
123
|
const importPath = options.process?.importPath ?? defaults.importPath;
|
package/dist/storage/lock.d.ts
CHANGED
|
@@ -2,4 +2,9 @@ import { RunLockInfo } from "./types";
|
|
|
2
2
|
export declare function acquireRunLock(runDir: string, owner: string): Promise<RunLockInfo>;
|
|
3
3
|
export declare function releaseRunLock(runDir: string): Promise<void>;
|
|
4
4
|
export declare function readRunLock(runDir: string): Promise<RunLockInfo | null>;
|
|
5
|
+
export interface WithRunLockOptions {
|
|
6
|
+
retries?: number;
|
|
7
|
+
delayMs?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function withRunLock<T>(runDir: string, owner: string, fn: () => Promise<T>, options?: WithRunLockOptions): Promise<T>;
|
|
5
10
|
//# sourceMappingURL=lock.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lock.d.ts","sourceRoot":"","sources":["../../src/storage/lock.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAItC,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAcxF;AAED,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,iBAGlD;AAED,wBAAsB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAU7E"}
|
|
1
|
+
{"version":3,"file":"lock.d.ts","sourceRoot":"","sources":["../../src/storage/lock.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAItC,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAcxF;AAED,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,iBAGlD;AAED,wBAAsB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAU7E;AAWD,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAsB,WAAW,CAAC,CAAC,EACjC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,CAAC,CAAC,CAuBZ"}
|
package/dist/storage/lock.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.acquireRunLock = acquireRunLock;
|
|
4
4
|
exports.releaseRunLock = releaseRunLock;
|
|
5
5
|
exports.readRunLock = readRunLock;
|
|
6
|
+
exports.withRunLock = withRunLock;
|
|
6
7
|
const fs_1 = require("fs");
|
|
7
8
|
const paths_1 = require("./paths");
|
|
8
9
|
const clock_1 = require("./clock");
|
|
@@ -39,3 +40,37 @@ async function readRunLock(runDir) {
|
|
|
39
40
|
throw err;
|
|
40
41
|
}
|
|
41
42
|
}
|
|
43
|
+
function isLockHeldError(error) {
|
|
44
|
+
if (!(error instanceof Error))
|
|
45
|
+
return false;
|
|
46
|
+
return error.message.startsWith("run.lock already held");
|
|
47
|
+
}
|
|
48
|
+
async function sleep(ms) {
|
|
49
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
50
|
+
}
|
|
51
|
+
async function withRunLock(runDir, owner, fn, options = {}) {
|
|
52
|
+
const retries = typeof options.retries === "number" ? Math.max(0, Math.floor(options.retries)) : 40;
|
|
53
|
+
const delayMs = typeof options.delayMs === "number" ? Math.max(0, Math.floor(options.delayMs)) : 250;
|
|
54
|
+
let acquired = false;
|
|
55
|
+
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
56
|
+
try {
|
|
57
|
+
await acquireRunLock(runDir, owner);
|
|
58
|
+
acquired = true;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (!isLockHeldError(error) || attempt === retries) {
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
await sleep(delayMs);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
return await fn();
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
if (acquired) {
|
|
73
|
+
await releaseRunLock(runDir);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|