@a5c-ai/babysitter-sdk 0.0.40 → 0.0.42

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.
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":";AAmwCA,wBAAgB,mBAAmB;eAEf,MAAM,EAAE,GAA2B,OAAO,CAAC,MAAM,CAAC;kBAsCpD,MAAM;EAIvB"}
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":"AAEA,OAAO,EACL,2BAA2B,EAC3B,yBAAyB,EAG1B,MAAM,SAAS,CAAC;AAOjB,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,2BAA2B,CAAC,CAoEjH"}
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
- guardResultPayload(options);
13
- const effectIndex = await (0, effectIndex_1.buildEffectIndex)({ runDir: options.runDir });
14
- const record = effectIndex.getByEffectId(options.effectId);
15
- if (!record) {
16
- logCommitFailure(options, "unknown_effect");
17
- throw new exceptions_1.RunFailedError(`Unknown effectId ${options.effectId}`);
18
- }
19
- if (record.status !== "requested") {
20
- logCommitFailure(options, "already_resolved", { currentStatus: record.status });
21
- throw new exceptions_1.RunFailedError(`Effect ${options.effectId} is already resolved`);
22
- }
23
- ensureInvocationKeyMatches(options, record);
24
- const resultPayload = buildResultPayload(options);
25
- const { resultRef, stdoutRef: writtenStdoutRef, stderrRef: writtenStderrRef } = await (0, serializer_1.serializeAndWriteTaskResult)({
26
- runDir: options.runDir,
27
- effectId: options.effectId,
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
- status: options.result.status,
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":"AAYA,OAAO,EAEL,eAAe,EACf,kBAAkB,EAGnB,MAAM,SAAS,CAAC;AAYjB,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,eAAe,CAAC,CA2HhG"}
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
- const iterationStartedAt = Date.now();
21
- const nowFn = resolveNow(options.now);
22
- const engine = await initializeReplayEngine(options, nowFn, iterationStartedAt);
23
- const defaultEntrypoint = {
24
- importPath: engine.metadata.entrypoint?.importPath ?? engine.metadata.processPath,
25
- exportName: engine.metadata.entrypoint?.exportName,
26
- };
27
- const processFn = await loadProcessFunction(options, defaultEntrypoint, options.runDir);
28
- const inputs = options.inputs ?? engine.inputs;
29
- let finalStatus = "failed";
30
- const logger = engine.internalContext.logger ?? options.logger;
31
- // Compute project root for hook calls (parent of .a5c dir where plugins/ is located)
32
- // runDir is like: /path/to/project/.a5c/runs/<runId>
33
- // So we need 3 levels up: runs -> .a5c -> project
34
- const projectRoot = path_1.default.dirname(path_1.default.dirname(path_1.default.dirname(options.runDir)));
35
- // Call on-iteration-start hook
36
- await (0, runtime_1.callRuntimeHook)("on-iteration-start", {
37
- runId: engine.runId,
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
- status: "completed",
57
- output,
58
- duration: Date.now() - iterationStartedAt,
40
+ iteration: engine.replayCursor.value,
59
41
  }, {
60
42
  cwd: projectRoot,
61
43
  logger,
62
44
  });
63
- const result = { status: "completed", output, metadata: createIterationMetadata(engine) };
64
- finalStatus = result.status;
65
- return result;
66
- }
67
- catch (error) {
68
- const waiting = asWaitingResult(error);
69
- if (waiting) {
70
- finalStatus = waiting.status;
71
- return {
72
- status: "waiting",
73
- nextActions: annotateWaitingActions(waiting.nextActions),
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
- const failure = (0, errorUtils_1.serializeUnknownError)(error);
78
- await (0, journal_1.appendEvent)({
79
- runDir: options.runDir,
80
- eventType: "RUN_FAILED",
81
- event: { error: failure },
82
- });
83
- // Call on-run-fail hook
84
- await (0, runtime_1.callRuntimeHook)("on-run-fail", {
85
- runId: engine.runId,
86
- status: "failed",
87
- error: failure.message || "Unknown error",
88
- duration: Date.now() - iterationStartedAt,
89
- }, {
90
- cwd: projectRoot,
91
- logger,
92
- });
93
- const result = {
94
- status: "failed",
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;
@@ -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"}
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a5c-ai/babysitter-sdk",
3
- "version": "0.0.40",
3
+ "version": "0.0.42",
4
4
  "description": "Storage and run-registry primitives for event-sourced babysitter workflows.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "commonjs",