@bastani/atomic 0.9.42 → 0.9.43-alpha.1

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.
@@ -58,6 +58,113 @@ var __esm = (fn, res, err) => () => {
58
58
  };
59
59
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
60
60
 
61
+ // dist/builtin/workflows/src/durable/journal/types.ts
62
+ import { StorageSchemaVersionError } from "@bastani/atomic/storage";
63
+ import { StorageUnavailableError } from "@bastani/atomic/storage";
64
+ import { RUN_EVENT_TYPES } from "@bastani/atomic/storage";
65
+ var JOURNAL_SCHEMA_VERSION = 1, JOURNAL_PAYLOAD_ENCODING = "json-text", JOURNAL_TABLES, JOURNAL_COLUMNS, JOURNAL_LEASE, JournalError, JournalClosedError, JournalInvalidTextError, JournalMetaCorruptError, JournalCounterRangeError, StaleLeaseError, JournalCasConflictError, JournalSchemaVersionError, RunNotFoundError;
66
+ var init_types = __esm(() => {
67
+ JOURNAL_TABLES = { meta: "meta", runs: "runs", events: "events", leases: "leases" };
68
+ JOURNAL_COLUMNS = {
69
+ schemaVersion: "schema_version",
70
+ runId: "run_id",
71
+ name: "name",
72
+ cwd: "cwd",
73
+ createdAt: "created_at",
74
+ status: "status",
75
+ headerJson: "header_json",
76
+ seq: "seq",
77
+ epoch: "epoch",
78
+ key: "key",
79
+ type: "type",
80
+ payload: "payload",
81
+ at: "at",
82
+ holderId: "holder_id",
83
+ renewedAt: "renewed_at"
84
+ };
85
+ JOURNAL_LEASE = { initialEpoch: 1, epochIncrement: 1, unheld: null, absentEpoch: 0 };
86
+ JournalError = class JournalError extends Error {
87
+ };
88
+ JournalClosedError = class JournalClosedError extends JournalError {
89
+ constructor() {
90
+ super("Journal is closed");
91
+ this.name = "JournalClosedError";
92
+ }
93
+ };
94
+ JournalInvalidTextError = class JournalInvalidTextError extends JournalError {
95
+ field;
96
+ reason;
97
+ constructor(field, reason) {
98
+ super(`Invalid journal ${field}: ${reason}`);
99
+ this.name = "JournalInvalidTextError";
100
+ this.field = field;
101
+ this.reason = reason;
102
+ }
103
+ };
104
+ JournalMetaCorruptError = class JournalMetaCorruptError extends JournalError {
105
+ path;
106
+ rowCount;
107
+ constructor(path, rowCount) {
108
+ super(`Journal metadata at ${path} has ${rowCount} rows`);
109
+ this.name = "JournalMetaCorruptError";
110
+ this.path = path;
111
+ this.rowCount = rowCount;
112
+ }
113
+ };
114
+ JournalCounterRangeError = class JournalCounterRangeError extends JournalError {
115
+ runId;
116
+ field;
117
+ value;
118
+ reason;
119
+ constructor(runId, field, value2, reason) {
120
+ super(`Unsafe journal ${field} counter for run ${runId}: ${value2}`);
121
+ this.name = "JournalCounterRangeError";
122
+ this.runId = runId;
123
+ this.field = field;
124
+ this.value = value2;
125
+ this.reason = reason;
126
+ }
127
+ };
128
+ StaleLeaseError = class StaleLeaseError extends JournalError {
129
+ runId;
130
+ heldEpoch;
131
+ currentEpoch;
132
+ constructor(runId, heldEpoch, currentEpoch) {
133
+ super(`Stale lease for run ${runId}`);
134
+ this.name = "StaleLeaseError";
135
+ this.runId = runId;
136
+ this.heldEpoch = heldEpoch;
137
+ this.currentEpoch = currentEpoch;
138
+ }
139
+ };
140
+ JournalCasConflictError = class JournalCasConflictError extends JournalError {
141
+ runId;
142
+ expectedSeq;
143
+ actualSeq;
144
+ constructor(runId, expectedSeq, actualSeq) {
145
+ super(`Journal sequence conflict for run ${runId}`);
146
+ this.name = "JournalCasConflictError";
147
+ this.runId = runId;
148
+ this.expectedSeq = expectedSeq;
149
+ this.actualSeq = actualSeq;
150
+ }
151
+ };
152
+ JournalSchemaVersionError = class JournalSchemaVersionError extends StorageSchemaVersionError {
153
+ constructor(found, supported = JOURNAL_SCHEMA_VERSION) {
154
+ super("journal", found, supported);
155
+ this.name = "JournalSchemaVersionError";
156
+ }
157
+ };
158
+ RunNotFoundError = class RunNotFoundError extends JournalError {
159
+ runId;
160
+ constructor(runId) {
161
+ super(`Run not found: ${runId}`);
162
+ this.name = "RunNotFoundError";
163
+ this.runId = runId;
164
+ }
165
+ };
166
+ });
167
+
61
168
  // dist/builtin/workflows/src/durable/journal/internal.ts
62
169
  function registerRunJournalVersions(journal, versions) {
63
170
  versionReaders.set(journal, versions);
@@ -71,8 +178,14 @@ function registerSqlPoolStorage(journal, storage) {
71
178
  function sqlPoolStorage(journal) {
72
179
  return poolStorage.get(journal);
73
180
  }
74
- var JOURNAL_READ_PAGE_SIZE = 256, versionReaders, poolStorage;
181
+ var JOURNAL_READ_PAGE_SIZE = 256, NON_SNAPSHOT_EVENT_TYPES, versionReaders, poolStorage;
75
182
  var init_internal = __esm(() => {
183
+ init_types();
184
+ NON_SNAPSHOT_EVENT_TYPES = [
185
+ RUN_EVENT_TYPES.checkpointRecorded,
186
+ RUN_EVENT_TYPES.messageSettled,
187
+ RUN_EVENT_TYPES.messageDelivered
188
+ ];
76
189
  versionReaders = new WeakMap;
77
190
  poolStorage = new WeakMap;
78
191
  });
@@ -1280,113 +1393,6 @@ class PromptReservationState {
1280
1393
  }
1281
1394
  }
1282
1395
 
1283
- // dist/builtin/workflows/src/durable/journal/types.ts
1284
- import { StorageSchemaVersionError } from "@bastani/atomic/storage";
1285
- import { StorageUnavailableError } from "@bastani/atomic/storage";
1286
- import { RUN_EVENT_TYPES } from "@bastani/atomic/storage";
1287
- var JOURNAL_SCHEMA_VERSION = 1, JOURNAL_PAYLOAD_ENCODING = "json-text", JOURNAL_TABLES, JOURNAL_COLUMNS, JOURNAL_LEASE, JournalError, JournalClosedError, JournalInvalidTextError, JournalMetaCorruptError, JournalCounterRangeError, StaleLeaseError, JournalCasConflictError, JournalSchemaVersionError, RunNotFoundError;
1288
- var init_types = __esm(() => {
1289
- JOURNAL_TABLES = { meta: "meta", runs: "runs", events: "events", leases: "leases" };
1290
- JOURNAL_COLUMNS = {
1291
- schemaVersion: "schema_version",
1292
- runId: "run_id",
1293
- name: "name",
1294
- cwd: "cwd",
1295
- createdAt: "created_at",
1296
- status: "status",
1297
- headerJson: "header_json",
1298
- seq: "seq",
1299
- epoch: "epoch",
1300
- key: "key",
1301
- type: "type",
1302
- payload: "payload",
1303
- at: "at",
1304
- holderId: "holder_id",
1305
- renewedAt: "renewed_at"
1306
- };
1307
- JOURNAL_LEASE = { initialEpoch: 1, epochIncrement: 1, unheld: null, absentEpoch: 0 };
1308
- JournalError = class JournalError extends Error {
1309
- };
1310
- JournalClosedError = class JournalClosedError extends JournalError {
1311
- constructor() {
1312
- super("Journal is closed");
1313
- this.name = "JournalClosedError";
1314
- }
1315
- };
1316
- JournalInvalidTextError = class JournalInvalidTextError extends JournalError {
1317
- field;
1318
- reason;
1319
- constructor(field, reason) {
1320
- super(`Invalid journal ${field}: ${reason}`);
1321
- this.name = "JournalInvalidTextError";
1322
- this.field = field;
1323
- this.reason = reason;
1324
- }
1325
- };
1326
- JournalMetaCorruptError = class JournalMetaCorruptError extends JournalError {
1327
- path;
1328
- rowCount;
1329
- constructor(path, rowCount) {
1330
- super(`Journal metadata at ${path} has ${rowCount} rows`);
1331
- this.name = "JournalMetaCorruptError";
1332
- this.path = path;
1333
- this.rowCount = rowCount;
1334
- }
1335
- };
1336
- JournalCounterRangeError = class JournalCounterRangeError extends JournalError {
1337
- runId;
1338
- field;
1339
- value;
1340
- reason;
1341
- constructor(runId, field, value2, reason) {
1342
- super(`Unsafe journal ${field} counter for run ${runId}: ${value2}`);
1343
- this.name = "JournalCounterRangeError";
1344
- this.runId = runId;
1345
- this.field = field;
1346
- this.value = value2;
1347
- this.reason = reason;
1348
- }
1349
- };
1350
- StaleLeaseError = class StaleLeaseError extends JournalError {
1351
- runId;
1352
- heldEpoch;
1353
- currentEpoch;
1354
- constructor(runId, heldEpoch, currentEpoch) {
1355
- super(`Stale lease for run ${runId}`);
1356
- this.name = "StaleLeaseError";
1357
- this.runId = runId;
1358
- this.heldEpoch = heldEpoch;
1359
- this.currentEpoch = currentEpoch;
1360
- }
1361
- };
1362
- JournalCasConflictError = class JournalCasConflictError extends JournalError {
1363
- runId;
1364
- expectedSeq;
1365
- actualSeq;
1366
- constructor(runId, expectedSeq, actualSeq) {
1367
- super(`Journal sequence conflict for run ${runId}`);
1368
- this.name = "JournalCasConflictError";
1369
- this.runId = runId;
1370
- this.expectedSeq = expectedSeq;
1371
- this.actualSeq = actualSeq;
1372
- }
1373
- };
1374
- JournalSchemaVersionError = class JournalSchemaVersionError extends StorageSchemaVersionError {
1375
- constructor(found, supported = JOURNAL_SCHEMA_VERSION) {
1376
- super("journal", found, supported);
1377
- this.name = "JournalSchemaVersionError";
1378
- }
1379
- };
1380
- RunNotFoundError = class RunNotFoundError extends JournalError {
1381
- runId;
1382
- constructor(runId) {
1383
- super(`Run not found: ${runId}`);
1384
- this.name = "RunNotFoundError";
1385
- this.runId = runId;
1386
- }
1387
- };
1388
- });
1389
-
1390
1396
  // dist/builtin/workflows/src/durable/journal/values.ts
1391
1397
  function validateText(field, value2) {
1392
1398
  if (value2.includes("\x00"))
@@ -1690,6 +1696,7 @@ class JournalDurableBackend {
1690
1696
  projection = createJournalProjection();
1691
1697
  sequences = new Map;
1692
1698
  foldedAt = new Map;
1699
+ summarized = new Map;
1693
1700
  catalogToken;
1694
1701
  leases = new Map;
1695
1702
  executions = new Map;
@@ -1715,8 +1722,8 @@ class JournalDurableBackend {
1715
1722
  let backend;
1716
1723
  try {
1717
1724
  backend = new JournalDurableBackend(journal, undefined, options.ownsJournal ?? true);
1718
- for (const run of await journal.listRuns())
1719
- for await (const entry of journal.read(run.runId))
1725
+ for (const runId of options.runs ?? (await journal.listRuns()).map((run) => run.runId))
1726
+ for await (const entry of journal.read(runId))
1720
1727
  backend.fold(entry);
1721
1728
  return backend;
1722
1729
  } catch (error) {
@@ -1789,6 +1796,18 @@ class JournalDurableBackend {
1789
1796
  this.sequences.set(entry.runId, entry.seq);
1790
1797
  this.foldedAt.set(entry.runId, entry.at);
1791
1798
  }
1799
+ summarize(runId, tail) {
1800
+ const event = tail.snapshot.event;
1801
+ const handle = event.type === RUN_EVENT_TYPES.promptAsked || event.type === RUN_EVENT_TYPES.promptReleased ? event.payload.handle : event.payload;
1802
+ this.projection.deleteWorkflow(runId);
1803
+ this.prompts.delete(runId);
1804
+ this.projection.applyWorkflowSnapshot({
1805
+ ...handle,
1806
+ completedCheckpoints: handle.completedCheckpoints + tail.laterCheckpoints,
1807
+ ...tail.lastCheckpointAt === undefined ? {} : { updatedAt: tail.lastCheckpointAt }
1808
+ });
1809
+ this.summarized.set(runId, tail.version);
1810
+ }
1792
1811
  *entries(runId, fromSeq) {
1793
1812
  return yield* invoke(async () => {
1794
1813
  const entries = [];
@@ -1802,6 +1821,7 @@ class JournalDurableBackend {
1802
1821
  this.projection.deleteWorkflow(runId);
1803
1822
  this.sequences.delete(runId);
1804
1823
  this.foldedAt.delete(runId);
1824
+ this.summarized.delete(runId);
1805
1825
  this.prompts.delete(runId);
1806
1826
  for (const entry of entries)
1807
1827
  this.fold(entry);
@@ -2123,6 +2143,9 @@ class JournalDurableBackend {
2123
2143
  async hydrateWorkflow(runId) {
2124
2144
  await this.serialize(runId, () => this.rebuild(runId), false);
2125
2145
  }
2146
+ async catchUpWorkflow(runId) {
2147
+ await this.serialize(runId, () => this.extend(runId), false);
2148
+ }
2126
2149
  hydrateResumableWorkflows() {
2127
2150
  return this.serialize(this.catalogQueue, () => invoke(async () => {
2128
2151
  const journal = runJournalVersions(this.journal);
@@ -2136,16 +2159,33 @@ class JournalDurableBackend {
2136
2159
  if (token === this.catalogToken)
2137
2160
  return;
2138
2161
  const versions = await journal.runVersions();
2139
- for (const runId of new Set([...this.sequences.keys(), ...versions.keys()]))
2140
- await this.serialize(runId, () => this.catchUp(runId, versions.get(runId)), false);
2162
+ for (const runId of new Set([...this.sequences.keys(), ...this.summarized.keys(), ...versions.keys()]))
2163
+ await this.serialize(runId, () => this.catchUp(runId, versions.get(runId), journal), false);
2141
2164
  this.catalogToken = token;
2142
2165
  }));
2143
2166
  }
2144
- *catchUp(runId, version) {
2167
+ *catchUp(runId, version, journal) {
2145
2168
  const folded = this.sequences.get(runId) ?? 0;
2169
+ if (folded === 0 && version !== undefined) {
2170
+ const summarized = this.summarized.get(runId);
2171
+ if (summarized !== undefined && summarized.seq === version.seq && summarized.at === version.at)
2172
+ return;
2173
+ const tail = yield* invoke(() => journal.snapshotTail(runId));
2174
+ if (tail !== undefined)
2175
+ return this.summarize(runId, tail);
2176
+ if (summarized === undefined && version.seq === 0)
2177
+ return;
2178
+ return yield* this.rebuild(runId);
2179
+ }
2146
2180
  if (version !== undefined && version.seq === folded && version.at === (this.foldedAt.get(runId) ?? 0))
2147
2181
  return;
2148
- if (version !== undefined && folded > 0 && version.seq > folded) {
2182
+ if (version !== undefined && folded > 0 && version.seq > folded)
2183
+ return yield* this.extend(runId);
2184
+ yield* this.rebuild(runId);
2185
+ }
2186
+ *extend(runId) {
2187
+ const folded = this.sequences.get(runId) ?? 0;
2188
+ if (folded > 0) {
2149
2189
  const entries = yield* this.entries(runId, folded);
2150
2190
  if (entries[0]?.seq === folded && entries[0].at === this.foldedAt.get(runId)) {
2151
2191
  for (const entry of entries)
@@ -2173,6 +2213,7 @@ class JournalDurableBackend {
2173
2213
  this.leases.delete(runId);
2174
2214
  this.sequences.delete(runId);
2175
2215
  this.foldedAt.delete(runId);
2216
+ this.summarized.delete(runId);
2176
2217
  this.prompts.delete(runId);
2177
2218
  this.projection.deleteWorkflow(runId);
2178
2219
  }
@@ -2537,7 +2578,8 @@ class SqlRunJournal {
2537
2578
  this.legacy = legacy;
2538
2579
  registerRunJournalVersions(this, {
2539
2580
  changeToken: () => this.changeToken(),
2540
- runVersions: () => this.runVersions()
2581
+ runVersions: () => this.runVersions(),
2582
+ snapshotTail: (runId) => this.snapshotTail(runId)
2541
2583
  });
2542
2584
  registerSqlPoolStorage(this, {
2543
2585
  transaction: (body) => this.operation(() => this.database.transaction(body)),
@@ -2663,6 +2705,28 @@ class SqlRunJournal {
2663
2705
  ]));
2664
2706
  });
2665
2707
  }
2708
+ async snapshotTail(runId) {
2709
+ this.assertOpen();
2710
+ validateText("runId", runId);
2711
+ return this.operation(async () => {
2712
+ const events = await this.isUnconvertedLegacyRun(runId) ? JOURNAL_TABLES.events : this.tables.events;
2713
+ const later = `FROM ${events} AS later WHERE later.${JOURNAL_COLUMNS.runId} = snapshot.${JOURNAL_COLUMNS.runId} AND later.${JOURNAL_COLUMNS.seq} > snapshot.${JOURNAL_COLUMNS.seq} AND later.${JOURNAL_COLUMNS.type} = ?`;
2714
+ const row = await this.database.get(`SELECT ${EVENT_COLUMNS.map((column) => `snapshot.${column}`).join(", ")},
2715
+ latest.${JOURNAL_COLUMNS.seq} AS latest_seq, latest.${JOURNAL_COLUMNS.at} AS latest_at,
2716
+ (SELECT count(*) ${later}) AS later_checkpoints,
2717
+ (SELECT json_extract(later.${JOURNAL_COLUMNS.payload}, '$.completedAt') ${later} ORDER BY later.${JOURNAL_COLUMNS.seq} DESC LIMIT 1) AS last_checkpoint_at
2718
+ FROM (SELECT ${JOURNAL_COLUMNS.seq}, ${JOURNAL_COLUMNS.at} FROM ${events} WHERE ${JOURNAL_COLUMNS.runId} = ? ORDER BY ${JOURNAL_COLUMNS.seq} DESC LIMIT 1) AS latest,
2719
+ (SELECT ${EVENT_COLUMNS.join(", ")} FROM ${events} WHERE ${JOURNAL_COLUMNS.runId} = ? AND ${JOURNAL_COLUMNS.type} NOT IN (${NON_SNAPSHOT_EVENT_TYPES.map(() => "?").join(", ")}) ORDER BY ${JOURNAL_COLUMNS.seq} DESC LIMIT 1) AS snapshot`, RUN_EVENT_TYPES.checkpointRecorded, RUN_EVENT_TYPES.checkpointRecorded, runId, runId, ...NON_SNAPSHOT_EVENT_TYPES);
2720
+ if (row === undefined)
2721
+ return;
2722
+ return {
2723
+ version: { seq: journalCounter(runId, "seq", row.latest_seq), at: Number(row.latest_at) },
2724
+ snapshot: entryFromRow(row),
2725
+ laterCheckpoints: Number(row.later_checkpoints),
2726
+ ...row.last_checkpoint_at === null ? {} : { lastCheckpointAt: Number(row.last_checkpoint_at) }
2727
+ };
2728
+ });
2729
+ }
2666
2730
  async requireRun(tx, runId) {
2667
2731
  if (await tx.get(`SELECT ${JOURNAL_COLUMNS.runId} FROM ${this.tables.runs} WHERE ${JOURNAL_COLUMNS.runId} = ?`, runId))
2668
2732
  return;
@@ -2880,13 +2944,14 @@ class SqlRunJournal {
2880
2944
  return this.closePromise;
2881
2945
  }
2882
2946
  }
2883
- var STORE_NOW = "CAST(unixepoch('subsec') * 1000 AS INTEGER)";
2947
+ var STORE_NOW = "CAST(unixepoch('subsec') * 1000 AS INTEGER)", EVENT_COLUMNS;
2884
2948
  var init_sql_run_journal = __esm(() => {
2885
2949
  init_internal();
2886
2950
  init_legacy_journal_tables();
2887
2951
  init_types();
2888
2952
  init_values();
2889
2953
  init_internal();
2954
+ EVENT_COLUMNS = [JOURNAL_COLUMNS.runId, JOURNAL_COLUMNS.seq, JOURNAL_COLUMNS.epoch, JOURNAL_COLUMNS.key, JOURNAL_COLUMNS.type, JOURNAL_COLUMNS.payload, JOURNAL_COLUMNS.at];
2890
2955
  });
2891
2956
 
2892
2957
  // dist/builtin/workflows/src/durable/journal/resolution.ts
@@ -5679,7 +5744,13 @@ async function initializeDurableBackend(options = {}) {
5679
5744
  const selected = owner.selectedBackends;
5680
5745
  const existing = selected.get(key);
5681
5746
  const entry = existing ?? {
5682
- ready: openSelectedRunJournal(selection, agentDir).then((journal) => JournalDurableBackend.open(journal, { ownsJournal })).then((opened) => {
5747
+ ready: openSelectedRunJournal(selection, agentDir).then((journal) => JournalDurableBackend.open(journal, { ownsJournal, runs: [] })).then(async (opened) => {
5748
+ try {
5749
+ await opened.hydrateResumableWorkflows();
5750
+ } catch (error) {
5751
+ await opened.close();
5752
+ throw error;
5753
+ }
5683
5754
  if (ownsJournal)
5684
5755
  opened.closeOnProcessExit();
5685
5756
  entry.close = () => opened.close();
@@ -60525,6 +60596,57 @@ var init_types2 = __esm(() => {
60525
60596
  });
60526
60597
  });
60527
60598
 
60599
+ // dist/builtin/workflows/src/shared/workflow-infrastructure-error.ts
60600
+ function isInfrastructureError(value2) {
60601
+ if (value2 instanceof WorkflowInfrastructureError)
60602
+ return true;
60603
+ if (typeof value2 !== "object" || value2 === null)
60604
+ return false;
60605
+ const candidate = value2;
60606
+ return candidate.name === WORKFLOW_INFRASTRUCTURE_ERROR_NAME && candidate.retryable === true && typeof candidate.code === "string";
60607
+ }
60608
+ function isRetryableWorkflowError(error) {
60609
+ let current = error;
60610
+ for (let depth = 0;depth < MAX_CAUSE_DEPTH && current !== undefined && current !== null; depth++) {
60611
+ if (isInfrastructureError(current))
60612
+ return true;
60613
+ current = current instanceof Error ? current.cause : undefined;
60614
+ }
60615
+ return false;
60616
+ }
60617
+ function workflowInfrastructureFault(message) {
60618
+ return FAULT_PATTERNS.find(([pattern]) => pattern.test(message))?.[1];
60619
+ }
60620
+ function asWorkflowInfrastructureError(error) {
60621
+ if (isInfrastructureError(error))
60622
+ return error;
60623
+ if (!(error instanceof Error))
60624
+ return;
60625
+ const code = workflowInfrastructureFault(error.message);
60626
+ return code === undefined ? undefined : new WorkflowInfrastructureError(code, error.message, { cause: error });
60627
+ }
60628
+ var WORKFLOW_INFRASTRUCTURE_ERROR_NAME = "WorkflowInfrastructureError", MAX_CAUSE_DEPTH = 8, FAULT_PATTERNS, WorkflowInfrastructureError;
60629
+ var init_workflow_infrastructure_error = __esm(() => {
60630
+ FAULT_PATTERNS = [
60631
+ [/Local worker (?:process )?cleanup (?:was not confirmed|unconfirmed)/, "worker_cleanup_unconfirmed"],
60632
+ [/Local worker retry limit/, "worker_retry_limit"],
60633
+ [/Local worker (?:startup timed out|exited before readiness)/, "worker_startup_failed"],
60634
+ [/Worker exited before delivery acknowledgement/, "worker_lost"],
60635
+ [/Stale work lease/, "stale_work_lease"],
60636
+ [/Intercom runtime no longer active/, "intercom_unavailable"],
60637
+ [/Storage unavailable/, "storage_unavailable"]
60638
+ ];
60639
+ WorkflowInfrastructureError = class WorkflowInfrastructureError extends Error {
60640
+ name = WORKFLOW_INFRASTRUCTURE_ERROR_NAME;
60641
+ retryable = true;
60642
+ code;
60643
+ constructor(code, message, options) {
60644
+ super(message, options);
60645
+ this.code = code;
60646
+ }
60647
+ };
60648
+ });
60649
+
60528
60650
  // dist/builtin/workflows/src/workflows/registry.ts
60529
60651
  function makeRegistry(store) {
60530
60652
  return {
@@ -60596,11 +60718,13 @@ __export(exports_sdk_surface, {
60596
60718
  StaleLeaseError: () => StaleLeaseError,
60597
60719
  StorageUnavailableError: () => StorageUnavailableError,
60598
60720
  WORKFLOW_HEARTBEAT_CUSTOM_TYPE: () => WORKFLOW_HEARTBEAT_CUSTOM_TYPE,
60721
+ WorkflowInfrastructureError: () => WorkflowInfrastructureError,
60599
60722
  cancellationRegistry: () => cancellationRegistry,
60600
60723
  createAgentSessionAdapter: () => createAgentSessionAdapter,
60601
60724
  createCancellationRegistry: () => createCancellationRegistry,
60602
60725
  createRegistry: () => createRegistry,
60603
60726
  createStore: () => createStore,
60727
+ isRetryableWorkflowError: () => isRetryableWorkflowError,
60604
60728
  keepContext: () => keepContext,
60605
60729
  loadWorkerWorkflow: () => loadWorkerWorkflow,
60606
60730
  normalizeWorkflowName: () => normalizeWorkflowName,
@@ -60627,6 +60751,7 @@ var init_sdk_surface = __esm(() => {
60627
60751
  init_worktree();
60628
60752
  init_store();
60629
60753
  init_types2();
60754
+ init_workflow_infrastructure_error();
60630
60755
  init_definitions();
60631
60756
  init_registry();
60632
60757
  init_types();
@@ -61038,7 +61163,7 @@ class SqlPoolQueue {
61038
61163
  });
61039
61164
  const row = await this.current(tx, lease, token);
61040
61165
  if (row.epoch !== lease.epoch || row.expires_at < now)
61041
- throw new Error("Stale work lease");
61166
+ throw new Error(STALE_WORK_LEASE);
61042
61167
  assertSession();
61043
61168
  }, action);
61044
61169
  }
@@ -61047,7 +61172,7 @@ class SqlPoolQueue {
61047
61172
  const { now } = await this.authority(tx, lease);
61048
61173
  const row = await this.current(tx, lease, token);
61049
61174
  if (row.epoch !== lease.epoch || row.expires_at < now)
61050
- throw new Error("Stale work lease");
61175
+ throw new Error(STALE_WORK_LEASE);
61051
61176
  });
61052
61177
  }
61053
61178
  async enqueue(lease, descriptor, boundary) {
@@ -61127,7 +61252,7 @@ class SqlPoolQueue {
61127
61252
  async current(tx, lease, token, allowCompleted = false) {
61128
61253
  const row = await tx.get(`SELECT * FROM ${this.work} WHERE id = ? AND root = ?`, token.id, lease.runId);
61129
61254
  if (row?.state !== "leased" && !(allowCompleted && row?.state === "completed") || row.epoch !== token.epoch || row.generation !== token.generation || row.worker !== token.worker)
61130
- throw new Error("Stale work lease");
61255
+ throw new Error(STALE_WORK_LEASE);
61131
61256
  return row;
61132
61257
  }
61133
61258
  async renew(lease, token, operationId, durationMs) {
@@ -61140,7 +61265,7 @@ class SqlPoolQueue {
61140
61265
  const { now } = await this.authority(tx, lease);
61141
61266
  const row = await this.current(tx, lease, token);
61142
61267
  if (row.epoch !== lease.epoch || row.expires_at < now)
61143
- throw new Error("Stale work lease");
61268
+ throw new Error(STALE_WORK_LEASE);
61144
61269
  const previous = await tx.get(`SELECT * FROM ${this.operations} WHERE work = ? AND generation = ? AND operation = ?`, token.id, token.generation, operationId);
61145
61270
  if (previous) {
61146
61271
  if (previous.request !== request)
@@ -61168,7 +61293,7 @@ class SqlPoolQueue {
61168
61293
  const { now, status } = await this.authority(tx, lease);
61169
61294
  const row = await this.current(tx, lease, token);
61170
61295
  if (row.epoch !== lease.epoch)
61171
- throw new Error("Stale work lease");
61296
+ throw new Error(STALE_WORK_LEASE);
61172
61297
  const previous = await tx.get(`SELECT * FROM ${this.operations} WHERE work = ? AND generation = ? AND operation = ?`, token.id, token.generation, operationId);
61173
61298
  if (previous) {
61174
61299
  if (previous.request !== request)
@@ -61176,7 +61301,7 @@ class SqlPoolQueue {
61176
61301
  return JSON.parse(previous.receipt);
61177
61302
  }
61178
61303
  if (row.expires_at < now)
61179
- throw new Error("Stale work lease");
61304
+ throw new Error(STALE_WORK_LEASE);
61180
61305
  if (!await this.admitsAttemptWrites(tx, lease, status))
61181
61306
  throw new Error("Pool root is not running");
61182
61307
  const checkpoint = mutation.checkpoint;
@@ -61216,7 +61341,7 @@ class SqlPoolQueue {
61216
61341
  const { now, status } = await this.authority(tx, lease);
61217
61342
  const row = await this.current(tx, lease, token, true);
61218
61343
  if (row.epoch !== lease.epoch)
61219
- throw new Error("Stale work lease");
61344
+ throw new Error(STALE_WORK_LEASE);
61220
61345
  if (row.state === "completed") {
61221
61346
  const previous = await tx.get(`SELECT result FROM ${this.work}_results WHERE work = ?`, token.id);
61222
61347
  if (previous?.result !== payload)
@@ -61224,7 +61349,7 @@ class SqlPoolQueue {
61224
61349
  return;
61225
61350
  }
61226
61351
  if (row.expires_at < now || !await this.admitsAttemptWrites(tx, lease, status))
61227
- throw new Error("Stale work lease");
61352
+ throw new Error(STALE_WORK_LEASE);
61228
61353
  if (JSON.parse(payload).runId !== fromRow(row).childRunId)
61229
61354
  throw new Error("Pool result identity mismatch");
61230
61355
  await tx.run(`INSERT INTO ${this.work}_results VALUES (?, ?)`, token.id, payload);
@@ -61249,6 +61374,40 @@ class SqlPoolQueue {
61249
61374
  return true;
61250
61375
  });
61251
61376
  }
61377
+ async extend(lease, token, durationMs) {
61378
+ positive(durationMs);
61379
+ lease = snapshotLease(lease);
61380
+ token = { ...token };
61381
+ return this.transaction(async (tx) => {
61382
+ const { now } = await this.authority(tx, lease);
61383
+ const row = await this.current(tx, lease, token);
61384
+ if (row.epoch !== lease.epoch)
61385
+ throw new Error(STALE_WORK_LEASE);
61386
+ const expiresAt = Math.max(Number(row.expires_at), expiry(now, durationMs));
61387
+ await tx.run(`UPDATE ${this.work} SET expires_at = ?, renewed_at = ? WHERE id = ?`, expiresAt, now, row.id);
61388
+ return { ...fromRow(row), expiresAt, renewedAt: now };
61389
+ });
61390
+ }
61391
+ async releaseExited(lease, token) {
61392
+ lease = snapshotLease(lease);
61393
+ token = { ...token };
61394
+ return this.transaction(async (tx) => {
61395
+ await this.authority(tx, lease);
61396
+ const row = await tx.get(`SELECT * FROM ${this.work} WHERE id = ? AND root = ?`, token.id, lease.runId);
61397
+ if (row?.state !== "leased" || row.generation !== token.generation || row.worker !== token.worker || row.epoch !== token.epoch)
61398
+ return false;
61399
+ await tx.run(`UPDATE ${this.work} SET state = 'ready', worker = NULL, expires_at = 0 WHERE id = ?`, row.id);
61400
+ return true;
61401
+ });
61402
+ }
61403
+ async releasePreviousIncarnations(lease) {
61404
+ lease = snapshotLease(lease);
61405
+ return this.transaction(async (tx) => {
61406
+ await this.authority(tx, lease);
61407
+ const released = await tx.run(`UPDATE ${this.work} SET state = 'ready', worker = NULL, expires_at = 0 WHERE root = ? AND state = 'leased' AND epoch < ?`, lease.runId, lease.epoch);
61408
+ return released.changes;
61409
+ });
61410
+ }
61252
61411
  async inspect(root) {
61253
61412
  return this.transaction(async (tx) => (await tx.all(`SELECT * FROM ${this.work} WHERE root = ? ORDER BY ready_at, rowid`, root)).map(fromRow));
61254
61413
  }
@@ -61259,7 +61418,7 @@ function openSqlPoolQueue(journal) {
61259
61418
  return Promise.reject(new Error("Worker pool requires a SQL run journal"));
61260
61419
  return SqlPoolQueue.open(storage);
61261
61420
  }
61262
- var STORE_NOW2 = "CAST(unixepoch('subsec') * 1000 AS INTEGER)", MAX_WORKER_CLOCK_SKEW_MS = 1000;
61421
+ var STORE_NOW2 = "CAST(unixepoch('subsec') * 1000 AS INTEGER)", MAX_WORKER_CLOCK_SKEW_MS = 1000, STALE_WORK_LEASE = "Stale work lease";
61263
61422
  var init_sql_pool_queue = __esm(() => {
61264
61423
  init_serializable();
61265
61424
  init_boundary_topology();
@@ -61347,6 +61506,9 @@ var init_host_options = __esm(() => {
61347
61506
  hostWorkerBindings = Symbol("host-worker-bindings");
61348
61507
  });
61349
61508
 
61509
+ // dist/builtin/workflows/src/workers/protocol.ts
61510
+ var WORKER_HEARTBEAT_INTERVAL_MS = 1000;
61511
+
61350
61512
  // dist/builtin/workflows/src/runs/foreground/pending-stage-delivery.ts
61351
61513
  function isWorkflowPendingStageDeliveryFailure(error) {
61352
61514
  if (error instanceof WorkflowPendingStageDeliveryFailedError)
@@ -61886,20 +62048,33 @@ var init_stage_handoff = __esm(() => {
61886
62048
  // dist/builtin/workflows/src/workers/local-pool.ts
61887
62049
  import { fork } from "node:child_process";
61888
62050
  import { randomUUID as randomUUID16 } from "node:crypto";
61889
- import { resolve as resolve11 } from "node:path";
62051
+ import { createWriteStream } from "node:fs";
62052
+ import { mkdir as mkdir6, rm as rm3, rmdir } from "node:fs/promises";
62053
+ import { dirname as dirname12, join as join28, resolve as resolve11 } from "node:path";
61890
62054
  import { setTimeout as delay2 } from "node:timers/promises";
61891
62055
  import { fileURLToPath as fileURLToPath5 } from "node:url";
61892
62056
  import { isBunBinary as isBunBinary2 } from "@bastani/atomic";
62057
+ function workerLogDirectory(journalPath, root) {
62058
+ return join28(dirname12(resolve11(journalPath)), "worker-logs", encodeURIComponent(root).replaceAll(".", "%2E"));
62059
+ }
61893
62060
 
61894
62061
  class LocalWorkerPool {
61895
62062
  backend;
61896
62063
  options;
61897
62064
  children = new Set;
61898
62065
  incarnations = new Map;
62066
+ readyWorkers = new Set;
62067
+ logs = new Map;
62068
+ logDirectory;
61899
62069
  cleanups = new Set;
61900
62070
  recovering;
61901
62071
  nextRecoveryAt = 0;
61902
62072
  controlAcks = new Map;
62073
+ lastHeard = new Map;
62074
+ lastTick = performance.now();
62075
+ rootStalledMs = 0;
62076
+ ticker;
62077
+ leaseMs;
61903
62078
  desiredControl;
61904
62079
  root;
61905
62080
  store;
@@ -61923,9 +62098,15 @@ class LocalWorkerPool {
61923
62098
  this.options = options;
61924
62099
  if (!Number.isSafeInteger(options.workers) || options.workers < 1)
61925
62100
  throw new Error("Invalid local worker count");
62101
+ if (options.startupTimeoutMs !== undefined && (!Number.isSafeInteger(options.startupTimeoutMs) || options.startupTimeoutMs < 1 || options.startupTimeoutMs > MAX_TIMER_DELAY_MS))
62102
+ throw new Error("Invalid local worker startup timeout");
62103
+ if (options.leaseMs !== undefined && (!Number.isSafeInteger(options.leaseMs) || options.leaseMs < MINIMUM_WORK_LEASE_MS || options.leaseMs > MAX_TIMER_DELAY_MS))
62104
+ throw new Error(`Invalid local worker lease: must be an integer from ${MINIMUM_WORK_LEASE_MS} to ${MAX_TIMER_DELAY_MS} ms`);
62105
+ this.leaseMs = options.leaseMs ?? DEFAULT_WORK_LEASE_MS;
61926
62106
  this.releaseBinding = bindChildWorkerDispatcher(backend, this, options.rootId);
61927
62107
  if (options.admission && options.rootId === undefined)
61928
62108
  workflowResourceAdmission.set(backend, options.admission);
62109
+ this.ticker = setInterval(() => this.observeResponsiveness(), ROOT_TICK_INTERVAL_MS).unref();
61929
62110
  }
61930
62111
  get workerPids() {
61931
62112
  return [...this.children].flatMap((child) => child.pid === undefined ? [] : [child.pid]);
@@ -61951,6 +62132,7 @@ class LocalWorkerPool {
61951
62132
  this.lease = { runId: root, holderId: state.holderId, epoch: state.epoch };
61952
62133
  const queue = await openSqlPoolQueue(this.backend.journal);
61953
62134
  await queue.initialize(this.lease, COORDINATOR_LEASE_MS);
62135
+ await queue.releasePreviousIncarnations(this.lease);
61954
62136
  for (;; ) {
61955
62137
  const intended = this.desiredControl ?? "running";
61956
62138
  await queue.setControl(this.lease, intended);
@@ -61965,8 +62147,8 @@ class LocalWorkerPool {
61965
62147
  if (!this.queue || !this.lease)
61966
62148
  return;
61967
62149
  const control = await this.queue.setControl(this.lease, state);
61968
- const deadline = Date.now() + WORK_LEASE_MS;
61969
- while ([...this.incarnations.keys()].some((worker) => (this.controlAcks.get(worker) ?? -1) < control.revision)) {
62150
+ const deadline = Date.now() + CONTROL_ACKNOWLEDGEMENT_TIMEOUT_MS;
62151
+ while ([...this.readyWorkers].some((worker) => (this.controlAcks.get(worker) ?? -1) < control.revision)) {
61970
62152
  if (this.desiredControl !== state)
61971
62153
  return;
61972
62154
  if (Date.now() >= deadline) {
@@ -62036,7 +62218,16 @@ class LocalWorkerPool {
62036
62218
  root,
62037
62219
  holder: "coordinator"
62038
62220
  });
62039
- await Promise.all(Array.from({ length: this.options.workers }, () => this.spawn(root)));
62221
+ this.logDirectory = workerLogDirectory(this.options.journalPath, root);
62222
+ await mkdir6(this.logDirectory, { recursive: true }).catch(() => {
62223
+ return;
62224
+ });
62225
+ const workers = Array.from({ length: this.options.workers }, () => this.spawn(root));
62226
+ try {
62227
+ await Promise.any(workers);
62228
+ } catch (error) {
62229
+ throw error.errors[0];
62230
+ }
62040
62231
  }
62041
62232
  spawn(root) {
62042
62233
  if (this.closed)
@@ -62057,26 +62248,56 @@ class LocalWorkerPool {
62057
62248
  let serial = Promise.resolve();
62058
62249
  let work;
62059
62250
  const deliveries = new Map;
62251
+ const logPath = join28(this.logDirectory, `${worker}.log`);
62252
+ this.logs.set(child, logPath);
62253
+ let logFailure;
62254
+ const log = createWriteStream(logPath, { flags: "a" });
62255
+ log.on("error", (error) => {
62256
+ logFailure = error.message;
62257
+ });
62060
62258
  let stderr = "";
62061
62259
  child.stderr?.on("data", (chunk) => {
62062
- stderr = (stderr + chunk.toString()).slice(-8192);
62260
+ stderr = (stderr + chunk.toString()).slice(-STDERR_TAIL_CHARACTERS);
62261
+ });
62262
+ child.stderr?.on("error", (error) => {
62263
+ logFailure ??= error.message;
62264
+ log.end();
62063
62265
  });
62266
+ child.stderr?.pipe(log);
62267
+ const diagnosed = (summary) => new Error(`${summary} (worker stderr log: ${logFailure === undefined ? logPath : `unavailable, ${logFailure}`})${stderr ? `: ${stderr}` : ""}`);
62268
+ const startupTimeoutMs = this.options.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
62269
+ let timedOut = false;
62270
+ let terminated = false;
62271
+ const terminate = () => {
62272
+ if (terminated)
62273
+ return;
62274
+ terminated = true;
62275
+ child.kill("SIGKILL");
62276
+ };
62277
+ const replied = (error) => {
62278
+ if (error)
62279
+ terminate();
62280
+ };
62064
62281
  return new Promise((resolve, reject) => {
62065
62282
  const startup = setTimeout(() => {
62066
- this.failure = new Error("Local worker startup timed out");
62067
- reject(this.failure);
62068
- child.kill("SIGKILL");
62069
- }, 30000);
62070
- child.once("error", (error) => {
62283
+ timedOut = true;
62284
+ terminate();
62285
+ }, startupTimeoutMs);
62286
+ child.on("error", (error) => {
62071
62287
  clearTimeout(startup);
62072
62288
  reject(error);
62289
+ terminate();
62073
62290
  });
62291
+ const closed = new Promise((resolve) => child.once("close", () => resolve()));
62074
62292
  child.once("exit", () => {
62293
+ Promise.race([closed, delay2(STDERR_DRAIN_GRACE_MS)]).then(() => reject(diagnosed(timedOut ? `Local worker startup timed out after ${startupTimeoutMs} ms` : "Local worker exited before readiness")));
62075
62294
  this.children.delete(child);
62295
+ this.logs.delete(child);
62076
62296
  clearTimeout(startup);
62077
62297
  this.incarnations.delete(worker);
62078
- reject(new Error(`Local worker exited before readiness: ${stderr}`));
62298
+ this.readyWorkers.delete(worker);
62079
62299
  this.controlAcks.delete(worker);
62300
+ this.lastHeard.delete(worker);
62080
62301
  if (work)
62081
62302
  this.handoff?.fence(work);
62082
62303
  for (const delivery of deliveries.values())
@@ -62099,9 +62320,11 @@ class LocalWorkerPool {
62099
62320
  }
62100
62321
  if (this.closed)
62101
62322
  return;
62323
+ if (work)
62324
+ await this.queue.releaseExited(this.lease, work);
62102
62325
  const exhausted = work ? work.generation >= 3 : ++this.replacements > 3;
62103
62326
  if (exhausted)
62104
- this.failure = new Error(`Local worker retry limit: ${stderr}`);
62327
+ this.failure = diagnosed("Local worker retry limit");
62105
62328
  else
62106
62329
  await this.spawn(root);
62107
62330
  }).catch((error) => {
@@ -62111,6 +62334,8 @@ class LocalWorkerPool {
62111
62334
  cleanup.finally(() => this.cleanups.delete(cleanup));
62112
62335
  });
62113
62336
  child.on("message", (message) => {
62337
+ this.observeResponsiveness();
62338
+ this.lastHeard.set(worker, { at: performance.now(), rootStalled: this.rootStalledMs });
62114
62339
  if (message.kind === "delivery-ack") {
62115
62340
  const pending = deliveries.get(message.deliveryId);
62116
62341
  deliveries.delete(message.deliveryId);
@@ -62122,7 +62347,10 @@ class LocalWorkerPool {
62122
62347
  }
62123
62348
  const response = serial.then(async () => {
62124
62349
  if (message.kind === "ready") {
62350
+ if (terminated)
62351
+ return;
62125
62352
  ready = true;
62353
+ this.readyWorkers.add(worker);
62126
62354
  clearTimeout(startup);
62127
62355
  resolve();
62128
62356
  return;
@@ -62149,11 +62377,13 @@ class LocalWorkerPool {
62149
62377
  work = await queue.claim(lease, worker, this.sources.map((source) => ({
62150
62378
  definition: `${source.path}#${source.exportName}`,
62151
62379
  digest: source.digest
62152
- })), WORK_LEASE_MS);
62380
+ })), this.leaseMs);
62153
62381
  return work;
62154
62382
  }
62155
62383
  if (!work)
62156
62384
  throw new Error("Worker has no active attempt");
62385
+ if (message.kind === "renew" || work.expiresAt - Date.now() < this.leaseMs / 2)
62386
+ work = await queue.extend(lease, work, this.leaseMs);
62157
62387
  if (message.kind === "activity")
62158
62388
  return this.handoff?.publish(work, message.runs);
62159
62389
  if (message.kind === "route")
@@ -62174,10 +62404,8 @@ class LocalWorkerPool {
62174
62404
  });
62175
62405
  }));
62176
62406
  }
62177
- if (message.kind === "renew") {
62178
- work = await queue.renew(lease, work, randomUUID16(), WORK_LEASE_MS);
62407
+ if (message.kind === "renew")
62179
62408
  return;
62180
- }
62181
62409
  if (message.kind === "commit") {
62182
62410
  const receipt = await queue.commit(lease, work, message.operation, message.mutation);
62183
62411
  this.usageTotal = await this.usageLedger.total(root);
@@ -62202,13 +62430,7 @@ class LocalWorkerPool {
62202
62430
  });
62203
62431
  if (message.kind !== "delivery" && message.kind !== "delivery-ready")
62204
62432
  serial = response.then(() => {}, () => {});
62205
- response.then((value2) => {
62206
- if (child.connected)
62207
- child.send({ id: message.id, value: value2 });
62208
- }, (error) => {
62209
- if (child.connected)
62210
- child.send({ id: message.id, error: error.message });
62211
- });
62433
+ response.then((value2) => child.send({ id: message.id, value: value2 }, replied), (error) => child.send({ id: message.id, error: error.message }, replied));
62212
62434
  });
62213
62435
  });
62214
62436
  }
@@ -62218,14 +62440,22 @@ class LocalWorkerPool {
62218
62440
  if (this.closed || performance.now() < this.nextRecoveryAt)
62219
62441
  return Promise.resolve();
62220
62442
  this.recovering = (async () => {
62443
+ await new Promise((resolve) => setImmediate(resolve));
62221
62444
  await this.admission?.reconcile();
62222
62445
  const expiredBefore = Date.now();
62223
62446
  for (const work of await this.queue.inspect(root)) {
62224
62447
  if (work.state !== "leased" || work.expiresAt >= expiredBefore)
62225
62448
  continue;
62226
62449
  const child = work.worker === null ? undefined : this.incarnations.get(work.worker);
62227
- if (child)
62228
- await this.stop(child);
62450
+ if (child && work.worker !== null && this.isLive(work.worker, child)) {
62451
+ await this.queue.extend(this.lease, work, this.leaseMs).catch((error) => {
62452
+ if (error.message !== STALE_WORK_LEASE)
62453
+ throw error;
62454
+ });
62455
+ continue;
62456
+ }
62457
+ if (child && !await this.stop(child).then(() => true, () => false))
62458
+ continue;
62229
62459
  await this.queue.expire(this.lease, work);
62230
62460
  }
62231
62461
  })().finally(() => {
@@ -62234,6 +62464,18 @@ class LocalWorkerPool {
62234
62464
  });
62235
62465
  return this.recovering;
62236
62466
  }
62467
+ observeResponsiveness() {
62468
+ const now = performance.now();
62469
+ const gap = now - this.lastTick;
62470
+ if (gap > ROOT_STALL_THRESHOLD_MS)
62471
+ this.rootStalledMs += gap - ROOT_TICK_INTERVAL_MS;
62472
+ this.lastTick = now;
62473
+ }
62474
+ isLive(worker, child) {
62475
+ this.observeResponsiveness();
62476
+ const heard = this.lastHeard.get(worker);
62477
+ return child.exitCode === null && child.signalCode === null && child.connected && heard !== undefined && performance.now() - heard.at - (this.rootStalledMs - heard.rootStalled) < this.leaseMs;
62478
+ }
62237
62479
  stop(child) {
62238
62480
  if (child.pid === undefined || child.exitCode !== null || child.signalCode !== null)
62239
62481
  return Promise.resolve();
@@ -62250,9 +62492,13 @@ class LocalWorkerPool {
62250
62492
  try {
62251
62493
  return await this.dispatch(definition, inputs, options);
62252
62494
  } catch (error) {
62253
- if (this.desiredControl !== "quit" || !findWorkflowGracefulQuit(error))
62254
- await this.close();
62255
- throw error;
62495
+ try {
62496
+ if (this.desiredControl !== "quit" || !findWorkflowGracefulQuit(error))
62497
+ await this.close();
62498
+ } catch (closeError) {
62499
+ throw asWorkflowInfrastructureError(closeError) ?? closeError;
62500
+ }
62501
+ throw asWorkflowInfrastructureError(error) ?? error;
62256
62502
  }
62257
62503
  }
62258
62504
  async dispatch(definition, inputs, options) {
@@ -62296,6 +62542,8 @@ class LocalWorkerPool {
62296
62542
  for (;; ) {
62297
62543
  options.signal?.throwIfAborted();
62298
62544
  if (this.closed) {
62545
+ if (this.failure)
62546
+ throw this.failure;
62299
62547
  if (this.desiredControl === "quit")
62300
62548
  throw new WorkflowGracefulQuitError(root, "worker pool teardown");
62301
62549
  throw new Error("Local worker pool is closed");
@@ -62322,19 +62570,28 @@ class LocalWorkerPool {
62322
62570
  }
62323
62571
  async dispose() {
62324
62572
  this.closed = true;
62573
+ clearInterval(this.ticker);
62325
62574
  this.releaseBinding();
62326
62575
  if (this.options.rootId === undefined)
62327
62576
  workflowResourceAdmission.delete(this.backend);
62328
62577
  let failure;
62578
+ let stopping = [];
62329
62579
  try {
62330
62580
  await this.recovering?.catch(() => {});
62331
- await Promise.all([...this.children].map((child) => this.stop(child)));
62581
+ stopping = [...this.children].map((child) => ({ child, log: this.logs.get(child) }));
62582
+ await Promise.all(stopping.map(({ child }) => this.stop(child)));
62332
62583
  await Promise.all([...this.cleanups]);
62333
62584
  if (this.cleanupFailure)
62334
62585
  throw this.cleanupFailure;
62335
62586
  } catch (error) {
62336
62587
  failure = error;
62337
62588
  }
62589
+ if (failure === undefined) {
62590
+ const stopped = stopping.flatMap(({ child, log }) => log !== undefined && (child.exitCode !== null || child.signalCode !== null) ? [log] : []);
62591
+ await Promise.all(stopped.map((log) => rm3(log, { force: true }).catch(() => {})));
62592
+ if (this.logDirectory !== undefined)
62593
+ await rmdir(this.logDirectory).catch(() => {});
62594
+ }
62338
62595
  try {
62339
62596
  await this.releaseRoutes?.();
62340
62597
  } catch (error) {
@@ -62344,7 +62601,7 @@ class LocalWorkerPool {
62344
62601
  throw failure;
62345
62602
  }
62346
62603
  }
62347
- var INTERNAL_WORKFLOW_WORKER_ARG = "--internal-workflow-worker", WORK_LEASE_MS = 5000, RECOVERY_INTERVAL_MS = 500, COORDINATOR_LEASE_MS;
62604
+ var INTERNAL_WORKFLOW_WORKER_ARG = "--internal-workflow-worker", DEFAULT_WORK_LEASE_MS = 30000, MINIMUM_WORK_LEASE_MS, CONTROL_ACKNOWLEDGEMENT_TIMEOUT_MS = 5000, RECOVERY_INTERVAL_MS = 500, ROOT_TICK_INTERVAL_MS = 100, ROOT_STALL_THRESHOLD_MS = 500, COORDINATOR_LEASE_MS, DEFAULT_STARTUP_TIMEOUT_MS = 30000, MAX_TIMER_DELAY_MS = 2147483647, STDERR_TAIL_CHARACTERS = 8192, STDERR_DRAIN_GRACE_MS = 1000;
62348
62605
  var init_local_pool = __esm(() => {
62349
62606
  init_internal();
62350
62607
  init_sql_pool_queue();
@@ -62352,12 +62609,14 @@ var init_local_pool = __esm(() => {
62352
62609
  init_sql_run_journal();
62353
62610
  init_workflow_tool_abort();
62354
62611
  init_sdk_intercom();
62612
+ init_workflow_infrastructure_error();
62355
62613
  init_admission_binding();
62356
62614
  init_definitions();
62357
62615
  init_dispatch();
62358
62616
  init_host_options();
62359
62617
  init_resource_admission();
62360
62618
  init_stage_handoff();
62619
+ MINIMUM_WORK_LEASE_MS = 2 * WORKER_HEARTBEAT_INTERVAL_MS;
62361
62620
  COORDINATOR_LEASE_MS = 3 * STAGE_SESSION_HEARTBEAT_INTERVAL_MS;
62362
62621
  });
62363
62622
 
@@ -62399,6 +62658,8 @@ async function activatePublicPool(backend, options) {
62399
62658
  const cwd = options.cwd ?? process.cwd();
62400
62659
  const localWorkers = options.localWorkers && {
62401
62660
  ...options.localWorkers,
62661
+ startupTimeoutMs: undefined,
62662
+ leaseMs: undefined,
62402
62663
  definitions: options.localWorkers.definitions.map((source) => ({ ...source, path: resolve12(cwd, source.path) }))
62403
62664
  };
62404
62665
  const admission = options.admission && {
@@ -62427,7 +62688,9 @@ async function activatePublicPool(backend, options) {
62427
62688
  journalPath: backend.journal.path,
62428
62689
  workers: localWorkers.count,
62429
62690
  definitions,
62430
- admission: resources.admission ?? {}
62691
+ admission: resources.admission ?? {},
62692
+ startupTimeoutMs: options.localWorkers?.startupTimeoutMs,
62693
+ leaseMs: options.localWorkers?.leaseMs
62431
62694
  });
62432
62695
  try {
62433
62696
  pool.validateOptions(options);
@@ -63345,12 +63608,12 @@ function createChildWorkflowRunner(input) {
63345
63608
  const childExitStatus = isWorkflowExitStatus2(childRun.status) ? childRun.status : "blocked";
63346
63609
  if (!isWorkflowExitStatus2(childRun.status) && !childBudgetExceeded || childRun.status === "failed" && childRun.exited !== true) {
63347
63610
  const failedChildStage = childRun.stages.find((stage) => stage.failureKind !== undefined);
63348
- throw new Error(`atomic-workflows: child workflow "${childName}" (${child.name}) failed with status ${childRun.status}${childRun.error !== undefined ? `: ${childRun.error}` : ""}`, {
63349
- cause: {
63350
- ...failedChildStage?.failureKind !== undefined ? { code: failedChildStage.failureKind } : {},
63351
- ...failedChildStage?.failureMessage !== undefined ? { message: failedChildStage.failureMessage } : {}
63352
- }
63353
- });
63611
+ const message = `atomic-workflows: child workflow "${childName}" (${child.name}) failed with status ${childRun.status}${childRun.error !== undefined ? `: ${childRun.error}` : ""}`;
63612
+ const cause = {
63613
+ ...failedChildStage?.failureKind !== undefined ? { code: failedChildStage.failureKind } : {},
63614
+ ...failedChildStage?.failureMessage !== undefined ? { message: failedChildStage.failureMessage } : {}
63615
+ };
63616
+ throw new Error(message, { cause });
63354
63617
  }
63355
63618
  const outputs = selectWorkflowOutputs(child, childRun.result);
63356
63619
  const childExited = childBudgetExceeded || childRun.exited === true || childRun.status !== "completed";
@@ -63411,7 +63674,7 @@ function describeErrorWithCauses(error) {
63411
63674
  const messages = [];
63412
63675
  const visited = new Set;
63413
63676
  let current = error;
63414
- for (let depth = 0;current !== undefined && depth < MAX_CAUSE_DEPTH && !visited.has(current); depth++) {
63677
+ for (let depth = 0;current !== undefined && depth < MAX_CAUSE_DEPTH2 && !visited.has(current); depth++) {
63415
63678
  visited.add(current);
63416
63679
  const message = current instanceof Error ? current.message : String(current);
63417
63680
  if (!messages.includes(message))
@@ -63420,7 +63683,7 @@ function describeErrorWithCauses(error) {
63420
63683
  }
63421
63684
  return messages.join(": ");
63422
63685
  }
63423
- var MAX_CAUSE_DEPTH = 5;
63686
+ var MAX_CAUSE_DEPTH2 = 5;
63424
63687
 
63425
63688
  // dist/builtin/workflows/src/shared/returned-run-status.ts
63426
63689
  function normalizeReturnedWorkflowStatus(status) {
@@ -63747,7 +64010,7 @@ function hasUsableContentBlock(block) {
63747
64010
 
63748
64011
  // dist/builtin/workflows/src/shared/stage-session-store.ts
63749
64012
  import { randomUUID as randomUUID18 } from "node:crypto";
63750
- import { dirname as dirname12, resolve as resolve13 } from "node:path";
64013
+ import { dirname as dirname13, resolve as resolve13 } from "node:path";
63751
64014
  import {
63752
64015
  JsonlSessionStore,
63753
64016
  resolveSessionStore,
@@ -63784,7 +64047,7 @@ async function migrateLegacyStageSession(store, sessionId, sessionFile) {
63784
64047
  if (sessionFile === undefined || await store.readHeader(sessionId) !== undefined)
63785
64048
  return;
63786
64049
  const location = resolve13(sessionFile);
63787
- const legacy = await JsonlSessionStore.open(dirname12(location), { exactDirectory: true, scanExisting: false });
64050
+ const legacy = await JsonlSessionStore.open(dirname13(location), { exactDirectory: true, scanExisting: false });
63788
64051
  try {
63789
64052
  for await (const file of legacy.scanFiles()) {
63790
64053
  if (resolve13(file.location) !== location || file.header.id !== sessionId)
@@ -64932,7 +65195,7 @@ function recordedOwners(boundary, reason) {
64932
65195
  const pending = [{ value: reason, depth: 0 }];
64933
65196
  while (pending.length > 0) {
64934
65197
  const { value: value2, depth } = pending.pop();
64935
- if (depth > MAX_CAUSE_DEPTH2)
65198
+ if (depth > MAX_CAUSE_DEPTH3)
64936
65199
  continue;
64937
65200
  if (isPrimitive(value2)) {
64938
65201
  if (depth === 0 || value2 !== undefined && value2 !== null) {
@@ -65254,7 +65517,7 @@ function containUiRejections(ui, scope) {
65254
65517
  custom: (factory, options) => scope.watch(ui.custom(factory, options))
65255
65518
  };
65256
65519
  }
65257
- var EVENT = "unhandledRejection", BOUNDARY_KEY, FLAG = "--unhandled-rejections", MAX_CAUSE_DEPTH2 = 8, MAX_PRIMITIVES_PER_RUN = 64, MAX_STOP_STRINGS = 64, ENDED_RUN_GRACE_MS = 30000, NATIVE_REPORTING_MODES, WATCHED_STAGE_METHODS;
65520
+ var EVENT = "unhandledRejection", BOUNDARY_KEY, FLAG = "--unhandled-rejections", MAX_CAUSE_DEPTH3 = 8, MAX_PRIMITIVES_PER_RUN = 64, MAX_STOP_STRINGS = 64, ENDED_RUN_GRACE_MS = 30000, NATIVE_REPORTING_MODES, WATCHED_STAGE_METHODS;
65258
65521
  var init_run_rejection_boundary = __esm(() => {
65259
65522
  init_prompt_callsite();
65260
65523
  init_workflow_source();
@@ -70047,8 +70310,8 @@ var init_stage_runner_controller = __esm(() => {
70047
70310
 
70048
70311
  // dist/builtin/workflows/src/runs/foreground/stage-runner-output.ts
70049
70312
  import { createHash as createHash4 } from "node:crypto";
70050
- import { mkdir as mkdir6, writeFile as writeFile10 } from "node:fs/promises";
70051
- import { basename as basename5, dirname as dirname13, isAbsolute as isAbsolute11, join as join28, resolve as resolve14 } from "node:path";
70313
+ import { mkdir as mkdir7, writeFile as writeFile10 } from "node:fs/promises";
70314
+ import { basename as basename5, dirname as dirname14, isAbsolute as isAbsolute11, join as join29, resolve as resolve14 } from "node:path";
70052
70315
  function normalizeMaxOutput2(maxOutput) {
70053
70316
  return {
70054
70317
  bytes: maxOutput?.bytes ?? DEFAULT_MAX_OUTPUT_BYTES,
@@ -70111,7 +70374,7 @@ function formatByteSize(bytes) {
70111
70374
  }
70112
70375
  function transcriptPath(runId, outputPath) {
70113
70376
  const digest = createHash4("sha256").update(outputPath).digest("hex").slice(0, 16);
70114
- return join28(workflowArtifactRunPath(runId), "transcripts", `${digest}-${basename5(outputPath)}.transcript.md`);
70377
+ return join29(workflowArtifactRunPath(runId), "transcripts", `${digest}-${basename5(outputPath)}.transcript.md`);
70115
70378
  }
70116
70379
  function appendContent(lines, content) {
70117
70380
  if (typeof content === "string") {
@@ -70231,7 +70494,7 @@ async function finalizePromptOutput(fullOutput, outputOptions, runtimeCwd, runId
70231
70494
  const transcriptFile = transcriptPath(runId, outputPath);
70232
70495
  const transcript = renderTranscript(messages, fullOutput);
70233
70496
  try {
70234
- await mkdir6(dirname13(outputPath), { recursive: true });
70497
+ await mkdir7(dirname14(outputPath), { recursive: true });
70235
70498
  await writeFile10(outputPath, fullOutput, "utf8");
70236
70499
  } catch (err) {
70237
70500
  return `${displayOutput}
@@ -70243,7 +70506,7 @@ ${err instanceof Error ? err.message : String(err)}`;
70243
70506
  let transcriptError;
70244
70507
  try {
70245
70508
  await ensureWorkflowArtifactRunDirectory(runId);
70246
- await mkdir6(dirname13(transcriptFile), { recursive: true });
70509
+ await mkdir7(dirname14(transcriptFile), { recursive: true });
70247
70510
  await writeFile10(transcriptFile, transcript, "utf8");
70248
70511
  transcriptAvailable = true;
70249
70512
  } catch (err) {
@@ -71152,7 +71415,7 @@ function reportsClosedSession(error) {
71152
71415
  }
71153
71416
  function isSessionClosedError(error) {
71154
71417
  let current = error;
71155
- for (let depth = 0;depth < MAX_CAUSE_DEPTH3 && current !== undefined; depth++) {
71418
+ for (let depth = 0;depth < MAX_CAUSE_DEPTH4 && current !== undefined; depth++) {
71156
71419
  try {
71157
71420
  if (reportsClosedSession(current))
71158
71421
  return true;
@@ -71163,7 +71426,7 @@ function isSessionClosedError(error) {
71163
71426
  }
71164
71427
  return false;
71165
71428
  }
71166
- var MAX_CAUSE_DEPTH3 = 8;
71429
+ var MAX_CAUSE_DEPTH4 = 8;
71167
71430
  var init_session_closed_error = () => {};
71168
71431
 
71169
71432
  // dist/builtin/workflows/src/engine/workflow-activity.ts
@@ -72381,7 +72644,7 @@ async function runLocalWorker(start) {
72381
72644
  return typeof value2 === "function" ? value2.bind(target) : value2;
72382
72645
  }
72383
72646
  });
72384
- const reader = await JournalDurableBackend.open(readOnlyJournal);
72647
+ const reader = await JournalDurableBackend.open(readOnlyJournal, { runs: [] });
72385
72648
  const sessions = await new StorageProvider3().openSessionStore({ kind: "sqlite", path: start.journalPath, prefix: `${sqlPoolStorage(journal).tables.runs}_pool_` }, { agentDir: "", domain: "sessions" });
72386
72649
  const commit = async (mutation) => {
72387
72650
  const receipt = await request({
@@ -72389,7 +72652,7 @@ async function runLocalWorker(start) {
72389
72652
  operation: randomUUID20(),
72390
72653
  mutation
72391
72654
  });
72392
- await reader.hydrateWorkflow(start.root);
72655
+ await reader.catchUpWorkflow(start.root);
72393
72656
  return receipt;
72394
72657
  };
72395
72658
  const reads = new Set([
@@ -72466,6 +72729,7 @@ async function runLocalWorker(start) {
72466
72729
  await delay3(25);
72467
72730
  continue;
72468
72731
  }
72732
+ await reader.catchUpWorkflow(start.root);
72469
72733
  const work = await request({ kind: "claim" });
72470
72734
  if (!work) {
72471
72735
  await delay3(25 + Math.random() * 25);
@@ -72474,7 +72738,7 @@ async function runLocalWorker(start) {
72474
72738
  const index = start.definitions.findIndex((source) => `${source.path}#${source.exportName}` === work.definition && source.digest === work.digest);
72475
72739
  if (index < 0)
72476
72740
  throw new Error("Worker definition compatibility error");
72477
- await reader.hydrateWorkflow(start.root);
72741
+ await reader.catchUpWorkflow(start.root);
72478
72742
  const controller = new AbortController;
72479
72743
  activeController = controller;
72480
72744
  const store = createStore();
@@ -72579,7 +72843,7 @@ async function runLocalWorker(start) {
72579
72843
  })().catch(failPulse).finally(() => {
72580
72844
  report = undefined;
72581
72845
  });
72582
- }, 1000);
72846
+ }, WORKER_HEARTBEAT_INTERVAL_MS);
72583
72847
  try {
72584
72848
  await observeControl();
72585
72849
  while ((await request({ kind: "control" })).state !== "running") {