@adonis-agora/durable 0.58.3 → 0.60.0

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.
Files changed (219) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/dist/assets/spa/assets/index-70hGzOnT.css +1 -0
  3. package/dist/assets/spa/assets/index-BI1EANJO.js +16 -0
  4. package/dist/assets/spa/assets/index-BI1EANJO.js.map +1 -0
  5. package/dist/assets/spa/index.html +2 -2
  6. package/dist/commands/lifecycle_republish.d.ts +23 -0
  7. package/dist/commands/lifecycle_republish.d.ts.map +1 -0
  8. package/dist/commands/lifecycle_republish.js +64 -0
  9. package/dist/commands/lifecycle_republish.js.map +1 -0
  10. package/dist/commands/main.d.ts +4 -3
  11. package/dist/commands/main.d.ts.map +1 -1
  12. package/dist/commands/main.js +3 -1
  13. package/dist/commands/main.js.map +1 -1
  14. package/dist/providers/dashboard_provider.d.ts +2 -0
  15. package/dist/providers/dashboard_provider.d.ts.map +1 -1
  16. package/dist/providers/dashboard_provider.js +9 -1
  17. package/dist/providers/dashboard_provider.js.map +1 -1
  18. package/dist/providers/durable_provider.d.ts.map +1 -1
  19. package/dist/providers/durable_provider.js +3 -0
  20. package/dist/providers/durable_provider.js.map +1 -1
  21. package/dist/src/codec-state-store.d.ts +10 -1
  22. package/dist/src/codec-state-store.d.ts.map +1 -1
  23. package/dist/src/codec-state-store.js +25 -0
  24. package/dist/src/codec-state-store.js.map +1 -1
  25. package/dist/src/commands/index.d.ts +1 -1
  26. package/dist/src/commands/index.d.ts.map +1 -1
  27. package/dist/src/commands/index.js +1 -1
  28. package/dist/src/commands/index.js.map +1 -1
  29. package/dist/src/commands/runs.d.ts +24 -1
  30. package/dist/src/commands/runs.d.ts.map +1 -1
  31. package/dist/src/commands/runs.js +50 -0
  32. package/dist/src/commands/runs.js.map +1 -1
  33. package/dist/src/commands/worker.d.ts.map +1 -1
  34. package/dist/src/commands/worker.js +3 -0
  35. package/dist/src/commands/worker.js.map +1 -1
  36. package/dist/src/dashboard/gateway-adapter.d.ts +5 -0
  37. package/dist/src/dashboard/gateway-adapter.d.ts.map +1 -1
  38. package/dist/src/dashboard/gateway-adapter.js +20 -0
  39. package/dist/src/dashboard/gateway-adapter.js.map +1 -1
  40. package/dist/src/dashboard/handlers.d.ts +29 -1
  41. package/dist/src/dashboard/handlers.d.ts.map +1 -1
  42. package/dist/src/dashboard/handlers.js +103 -0
  43. package/dist/src/dashboard/handlers.js.map +1 -1
  44. package/dist/src/dashboard/index.d.ts +2 -2
  45. package/dist/src/dashboard/index.d.ts.map +1 -1
  46. package/dist/src/dashboard/index.js +2 -2
  47. package/dist/src/dashboard/index.js.map +1 -1
  48. package/dist/src/dashboard/openapi.d.ts.map +1 -1
  49. package/dist/src/dashboard/openapi.js +39 -0
  50. package/dist/src/dashboard/openapi.js.map +1 -1
  51. package/dist/src/define_config.d.ts +13 -0
  52. package/dist/src/define_config.d.ts.map +1 -1
  53. package/dist/src/define_config.js.map +1 -1
  54. package/dist/src/diagnostics-bridge.d.ts +5 -1
  55. package/dist/src/diagnostics-bridge.d.ts.map +1 -1
  56. package/dist/src/diagnostics-bridge.js +16 -2
  57. package/dist/src/diagnostics-bridge.js.map +1 -1
  58. package/dist/src/engine.d.ts +426 -6
  59. package/dist/src/engine.d.ts.map +1 -1
  60. package/dist/src/engine.js +1951 -101
  61. package/dist/src/engine.js.map +1 -1
  62. package/dist/src/errors.d.ts.map +1 -1
  63. package/dist/src/errors.js +4 -0
  64. package/dist/src/errors.js.map +1 -1
  65. package/dist/src/event-accumulators.d.ts +28 -5
  66. package/dist/src/event-accumulators.d.ts.map +1 -1
  67. package/dist/src/event-accumulators.js +102 -21
  68. package/dist/src/event-accumulators.js.map +1 -1
  69. package/dist/src/events.d.ts +25 -0
  70. package/dist/src/events.d.ts.map +1 -1
  71. package/dist/src/events.js +24 -0
  72. package/dist/src/events.js.map +1 -1
  73. package/dist/src/handshake/announced.d.ts +6 -0
  74. package/dist/src/handshake/announced.d.ts.map +1 -1
  75. package/dist/src/handshake/announced.js +20 -0
  76. package/dist/src/handshake/announced.js.map +1 -1
  77. package/dist/src/handshake/descriptor.d.ts +6 -0
  78. package/dist/src/handshake/descriptor.d.ts.map +1 -1
  79. package/dist/src/handshake/descriptor.js +1 -0
  80. package/dist/src/handshake/descriptor.js.map +1 -1
  81. package/dist/src/index.d.ts +3 -2
  82. package/dist/src/index.d.ts.map +1 -1
  83. package/dist/src/index.js +3 -2
  84. package/dist/src/index.js.map +1 -1
  85. package/dist/src/interfaces.d.ts +139 -3
  86. package/dist/src/interfaces.d.ts.map +1 -1
  87. package/dist/src/interfaces.js +11 -0
  88. package/dist/src/interfaces.js.map +1 -1
  89. package/dist/src/lifecycle-events.d.ts +372 -0
  90. package/dist/src/lifecycle-events.d.ts.map +1 -0
  91. package/dist/src/lifecycle-events.js +383 -0
  92. package/dist/src/lifecycle-events.js.map +1 -0
  93. package/dist/src/metrics.d.ts +15 -0
  94. package/dist/src/metrics.d.ts.map +1 -1
  95. package/dist/src/metrics.js +36 -1
  96. package/dist/src/metrics.js.map +1 -1
  97. package/dist/src/otel/durable-metrics.d.ts +9 -0
  98. package/dist/src/otel/durable-metrics.d.ts.map +1 -1
  99. package/dist/src/otel/durable-metrics.js +17 -0
  100. package/dist/src/otel/durable-metrics.js.map +1 -1
  101. package/dist/src/otel/durable-otel.d.ts +7 -0
  102. package/dist/src/otel/durable-otel.d.ts.map +1 -1
  103. package/dist/src/otel/durable-otel.js +69 -3
  104. package/dist/src/otel/durable-otel.js.map +1 -1
  105. package/dist/src/run-detail-summary.d.ts +48 -1
  106. package/dist/src/run-detail-summary.d.ts.map +1 -1
  107. package/dist/src/run-detail-summary.js +30 -2
  108. package/dist/src/run-detail-summary.js.map +1 -1
  109. package/dist/src/run-gateway/interface.d.ts +16 -0
  110. package/dist/src/run-gateway/interface.d.ts.map +1 -1
  111. package/dist/src/run-gateway/proxy-run-gateway.d.ts +31 -1
  112. package/dist/src/run-gateway/proxy-run-gateway.d.ts.map +1 -1
  113. package/dist/src/run-gateway/proxy-run-gateway.js +59 -7
  114. package/dist/src/run-gateway/proxy-run-gateway.js.map +1 -1
  115. package/dist/src/run-gateway/run-request-responder.d.ts +4 -29
  116. package/dist/src/run-gateway/run-request-responder.d.ts.map +1 -1
  117. package/dist/src/run-gateway/run-request-responder.js +183 -6
  118. package/dist/src/run-gateway/run-request-responder.js.map +1 -1
  119. package/dist/src/run-gateway/store-run-gateway.d.ts +9 -0
  120. package/dist/src/run-gateway/store-run-gateway.d.ts.map +1 -1
  121. package/dist/src/run-gateway/store-run-gateway.js +8 -0
  122. package/dist/src/run-gateway/store-run-gateway.js.map +1 -1
  123. package/dist/src/run-waiting.d.ts +1 -0
  124. package/dist/src/run-waiting.d.ts.map +1 -1
  125. package/dist/src/run-waiting.js +6 -1
  126. package/dist/src/run-waiting.js.map +1 -1
  127. package/dist/src/sticky-continuation.d.ts +141 -0
  128. package/dist/src/sticky-continuation.d.ts.map +1 -0
  129. package/dist/src/sticky-continuation.js +231 -0
  130. package/dist/src/sticky-continuation.js.map +1 -0
  131. package/dist/src/stores/factory.d.ts +6 -0
  132. package/dist/src/stores/factory.d.ts.map +1 -1
  133. package/dist/src/stores/factory.js +3 -0
  134. package/dist/src/stores/factory.js.map +1 -1
  135. package/dist/src/stores/lucid-pg-hot-path.d.ts +30 -0
  136. package/dist/src/stores/lucid-pg-hot-path.d.ts.map +1 -0
  137. package/dist/src/stores/lucid-pg-hot-path.js +67 -0
  138. package/dist/src/stores/lucid-pg-hot-path.js.map +1 -0
  139. package/dist/src/stores/lucid.d.ts +30 -1
  140. package/dist/src/stores/lucid.d.ts.map +1 -1
  141. package/dist/src/stores/lucid.js +460 -17
  142. package/dist/src/stores/lucid.js.map +1 -1
  143. package/dist/src/telescope/dashboard.d.ts.map +1 -1
  144. package/dist/src/telescope/dashboard.js +50 -0
  145. package/dist/src/telescope/dashboard.js.map +1 -1
  146. package/dist/src/telescope/data-providers.d.ts +19 -0
  147. package/dist/src/telescope/data-providers.d.ts.map +1 -1
  148. package/dist/src/telescope/data-providers.js +128 -2
  149. package/dist/src/telescope/data-providers.js.map +1 -1
  150. package/dist/src/telescope/extension.d.ts.map +1 -1
  151. package/dist/src/telescope/extension.js +4 -1
  152. package/dist/src/telescope/extension.js.map +1 -1
  153. package/dist/src/telescope/index.d.ts +3 -2
  154. package/dist/src/telescope/index.d.ts.map +1 -1
  155. package/dist/src/telescope/index.js +2 -1
  156. package/dist/src/telescope/index.js.map +1 -1
  157. package/dist/src/telescope/telescope-sdk.d.ts +16 -0
  158. package/dist/src/telescope/telescope-sdk.d.ts.map +1 -1
  159. package/dist/src/telescope/watcher.d.ts +66 -0
  160. package/dist/src/telescope/watcher.d.ts.map +1 -0
  161. package/dist/src/telescope/watcher.js +185 -0
  162. package/dist/src/telescope/watcher.js.map +1 -0
  163. package/dist/src/testing/in-memory-state-store.d.ts +12 -1
  164. package/dist/src/testing/in-memory-state-store.d.ts.map +1 -1
  165. package/dist/src/testing/in-memory-state-store.js +109 -1
  166. package/dist/src/testing/in-memory-state-store.js.map +1 -1
  167. package/dist/src/testing-kit/harness.d.ts +1 -1
  168. package/dist/src/testing-kit/harness.d.ts.map +1 -1
  169. package/dist/src/testing-kit/harness.js +3 -1
  170. package/dist/src/testing-kit/harness.js.map +1 -1
  171. package/dist/src/testing-kit/state-store-conformance.d.ts.map +1 -1
  172. package/dist/src/testing-kit/state-store-conformance.js +258 -0
  173. package/dist/src/testing-kit/state-store-conformance.js.map +1 -1
  174. package/dist/src/transport-pool.d.ts +10 -0
  175. package/dist/src/transport-pool.d.ts.map +1 -1
  176. package/dist/src/transport-pool.js +23 -2
  177. package/dist/src/transport-pool.js.map +1 -1
  178. package/dist/src/transports/bullmq/bullmq-transport.d.ts +41 -1
  179. package/dist/src/transports/bullmq/bullmq-transport.d.ts.map +1 -1
  180. package/dist/src/transports/bullmq/bullmq-transport.js +283 -5
  181. package/dist/src/transports/bullmq/bullmq-transport.js.map +1 -1
  182. package/dist/src/transports/bullmq/deps.d.ts +12 -1
  183. package/dist/src/transports/bullmq/deps.d.ts.map +1 -1
  184. package/dist/src/transports/bullmq/deps.js.map +1 -1
  185. package/dist/src/transports/bullmq/naming.d.ts +10 -0
  186. package/dist/src/transports/bullmq/naming.d.ts.map +1 -1
  187. package/dist/src/transports/bullmq/naming.js +16 -0
  188. package/dist/src/transports/bullmq/naming.js.map +1 -1
  189. package/dist/src/transports/db.d.ts +3 -1
  190. package/dist/src/transports/db.d.ts.map +1 -1
  191. package/dist/src/transports/db.js +4 -4
  192. package/dist/src/transports/db.js.map +1 -1
  193. package/dist/src/transports/queue.d.ts +3 -1
  194. package/dist/src/transports/queue.d.ts.map +1 -1
  195. package/dist/src/transports/queue.js +2 -2
  196. package/dist/src/transports/queue.js.map +1 -1
  197. package/dist/src/transports/worker-keys.d.ts +2 -1
  198. package/dist/src/transports/worker-keys.d.ts.map +1 -1
  199. package/dist/src/transports/worker-keys.js +3 -2
  200. package/dist/src/transports/worker-keys.js.map +1 -1
  201. package/dist/src/workflow-ctx.d.ts +7 -0
  202. package/dist/src/workflow-ctx.d.ts.map +1 -1
  203. package/dist/src/workflow-ctx.js +90 -1
  204. package/dist/src/workflow-ctx.js.map +1 -1
  205. package/dist/src/workflow-discovery.d.ts.map +1 -1
  206. package/dist/src/workflow-discovery.js +1 -0
  207. package/dist/src/workflow-discovery.js.map +1 -1
  208. package/dist/src/workflow-events.d.ts +101 -0
  209. package/dist/src/workflow-events.d.ts.map +1 -1
  210. package/dist/src/workflow-events.js +79 -1
  211. package/dist/src/workflow-events.js.map +1 -1
  212. package/dist/src/workflow-ref.d.ts +17 -2
  213. package/dist/src/workflow-ref.d.ts.map +1 -1
  214. package/dist/src/workflow-ref.js.map +1 -1
  215. package/package.json +5 -5
  216. package/skills/durable-cluster/SKILL.md +4 -2
  217. package/dist/assets/spa/assets/index-6nmSTfT7.css +0 -1
  218. package/dist/assets/spa/assets/index-Dj0X-IAu.js +0 -16
  219. package/dist/assets/spa/assets/index-Dj0X-IAu.js.map +0 -1
@@ -1,3 +1,4 @@
1
+ import { SETTLEABLE_RUN_STATUSES } from '../interfaces.js';
1
2
  import { namespaceFilterValues, normalizeNamespace } from '../namespaces.js';
2
3
  import { mergeRunFacetRows } from '../run-facets.js';
3
4
  import { runPageWindow } from '../run-pagination.js';
@@ -5,6 +6,7 @@ import { axisIsRunColumn, mergeRunValueFacetRows, RUN_VALUE_FACET_LIMIT, RUN_VAL
5
6
  import { attributeColumnFor, attributeOperand, attributeOperands, normalizeAttributeRows, sqlComparator, } from '../search-attributes.js';
6
7
  import { SINGLETON_ACTIVE_STATUSES, SINGLETON_ADMITTED_TAG, settlesTerminally, singletonAdmissionDenialPatch, singletonAdmissionTags, withoutAdmission, } from '../singleton-admission.js';
7
8
  import { CHECKPOINT_COLUMNS_BUT_INPUT, checkpointToRow, parseJsonColumn, rowToCheckpoint, rowToRun, rowToSignalWaiter, runPatchToRow, runToRow, SETTLED_CHECKPOINT_COLUMNS, settlementToRow, } from './lucid-mappers.js';
9
+ import { LucidPgHotPath, quote } from './lucid-pg-hot-path.js';
8
10
  import { createDurableTables, DURABLE_TABLES } from './lucid-schema.js';
9
11
  /**
10
12
  * A production-grade, persistent `StateStore` backed by AdonisJS **Lucid** (Knex). Runs, checkpoints,
@@ -47,6 +49,16 @@ export class LucidStateStore {
47
49
  client() {
48
50
  return this.connectionName ? this.db.connection(this.connectionName) : this.db.connection();
49
51
  }
52
+ /**
53
+ * The Postgres hot path (see `lucid-pg-hot-path.ts`): the statements a turn and a step result run
54
+ * on every step, as constant SQL. `undefined` on any other dialect, or with Lucid's `debug` on.
55
+ * Resolved per call, like {@link client}: under a global transaction it is the transaction's.
56
+ */
57
+ hot() {
58
+ return LucidPgHotPath.for(this.client(), {
59
+ preparedStatements: this.options.preparedStatements,
60
+ });
61
+ }
50
62
  /** Idempotently provision the durable tables (called on boot when `autoSchema` is on). */
51
63
  async ensureSchema() {
52
64
  await createDurableTables(this.db, this.connectionName, this.options.collation !== undefined ? { collation: this.options.collation } : {});
@@ -56,6 +68,14 @@ export class LucidStateStore {
56
68
  if (normalizeAttributeRows(run.id, run.searchAttributes).length === 0) {
57
69
  // Nothing to index: the insert, then clear any attribute rows a deleted run of this id left,
58
70
  // as two statements. The insert goes first, so a run that already exists keeps its rows.
71
+ const hot = this.hot();
72
+ if (hot) {
73
+ const row = runToRow(run);
74
+ const keys = Object.keys(row);
75
+ await hot.run(insertSql(DURABLE_TABLES.runs, keys), keys.map((k) => row[k]));
76
+ await hot.run(DELETE_ATTRIBUTES_SQL, [run.id]);
77
+ return;
78
+ }
59
79
  const client = this.client();
60
80
  await client.table(DURABLE_TABLES.runs).insert(runToRow(run));
61
81
  await client.from(DURABLE_TABLES.attributes).where('run_id', run.id).delete();
@@ -77,6 +97,15 @@ export class LucidStateStore {
77
97
  return this.client().transaction((trx) => write(trx));
78
98
  }
79
99
  async updateRun(runId, patch) {
100
+ const hot = this.hotFor(patch);
101
+ if (hot) {
102
+ const row = await this.releaseSlot(this.client(), runId, patch);
103
+ const keys = Object.keys(row);
104
+ if (keys.length) {
105
+ await hot.run(updateRunSql(keys, false), [...keys.map((k) => row[k]), runId]);
106
+ }
107
+ return;
108
+ }
80
109
  await this.writeRun(patch, async (trx) => {
81
110
  const row = await this.releaseSlot(trx, runId, patch);
82
111
  // Knex throws on an empty `.update({})`; skip the UPDATE when no mapped column changed.
@@ -89,6 +118,21 @@ export class LucidStateStore {
89
118
  });
90
119
  }
91
120
  async updateRunIf(runId, expectedStatuses, patch) {
121
+ const hot = this.hotFor(patch);
122
+ if (hot) {
123
+ const row = await this.releaseSlot(this.client(), runId, patch);
124
+ const keys = Object.keys(row);
125
+ if (keys.length === 0) {
126
+ const found = await hot.rows(RUN_IN_STATUSES_SQL, [runId, expectedStatuses]);
127
+ return found.length > 0;
128
+ }
129
+ const { count } = await hot.run(updateRunSql(keys, true), [
130
+ ...keys.map((k) => row[k]),
131
+ runId,
132
+ expectedStatuses,
133
+ ]);
134
+ return count === 1;
135
+ }
92
136
  return this.writeRun(patch, async (trx) => {
93
137
  const row = await this.releaseSlot(trx, runId, patch);
94
138
  let applied;
@@ -119,6 +163,15 @@ export class LucidStateStore {
119
163
  return applied;
120
164
  });
121
165
  }
166
+ /**
167
+ * The hot path for a run write of `patch`: one that needs no transaction (see {@link writeRun}),
168
+ * on a connection that takes it.
169
+ */
170
+ hotFor(patch) {
171
+ if (settlesTerminally(patch) || 'searchAttributes' in patch)
172
+ return undefined;
173
+ return this.hot();
174
+ }
122
175
  async pullRunWakesIn(runIds, wakeAt) {
123
176
  if (runIds.length === 0)
124
177
  return;
@@ -222,6 +275,13 @@ export class LucidStateStore {
222
275
  });
223
276
  }
224
277
  async getRun(runId) {
278
+ const hot = this.hot();
279
+ if (hot) {
280
+ const rows = this.scopeNamespace === undefined
281
+ ? await hot.rows(GET_RUN_SQL, [runId])
282
+ : await hot.rows(GET_RUN_SCOPED_SQL, [runId, namespaceFilterValues(this.scopeNamespace)]);
283
+ return rows[0] ? rowToRun(rows[0]) : null;
284
+ }
225
285
  const row = await this.fenced(this.client().from(DURABLE_TABLES.runs).where('id', runId)).first();
226
286
  return row ? rowToRun(row) : null;
227
287
  }
@@ -261,6 +321,11 @@ export class LucidStateStore {
261
321
  }
262
322
  // --- checkpoints --------------------------------------------------------
263
323
  async getCheckpoint(runId, seq) {
324
+ const hot = this.hot();
325
+ if (hot) {
326
+ const rows = await hot.rows(GET_CHECKPOINT_SQL, [runId, seq]);
327
+ return rows[0] ? rowToCheckpoint(rows[0]) : null;
328
+ }
264
329
  const row = await this.client()
265
330
  .from(DURABLE_TABLES.checkpoints)
266
331
  .where('run_id', runId)
@@ -282,6 +347,13 @@ export class LucidStateStore {
282
347
  }
283
348
  async getCheckpointsForSettle(runId, seqs) {
284
349
  const unique = [...new Set(seqs)];
350
+ const hot = this.hot();
351
+ if (hot && unique.length <= CHECKPOINT_BATCH) {
352
+ const rows = await hot.rows(CHECKPOINTS_FOR_SETTLE_SQL, [runId, unique]);
353
+ return rows
354
+ .map(rowToCheckpoint)
355
+ .sort((a, b) => a.seq - b.seq);
356
+ }
285
357
  const out = [];
286
358
  for (let at = 0; at < unique.length; at += CHECKPOINT_BATCH) {
287
359
  const rows = (await this.client()
@@ -408,6 +480,135 @@ export class LucidStateStore {
408
480
  return wrote;
409
481
  });
410
482
  }
483
+ async settleResultsIfPending(results) {
484
+ if (results.length === 0)
485
+ return [];
486
+ // A later entry for a row an earlier one claims loses, before anything is written.
487
+ const seen = new Set();
488
+ const first = results.map((r) => {
489
+ const k = checkpointKey(r.runId, r.seq);
490
+ if (seen.has(k))
491
+ return false;
492
+ seen.add(k);
493
+ return true;
494
+ });
495
+ const live = results.filter((_, i) => first[i]);
496
+ const client = this.client();
497
+ const settled = client.dialect.name === 'postgres'
498
+ ? await settleResultsReturningPg(client, this.hot(), live)
499
+ : await this.settleResultsPlain(live);
500
+ const out = [];
501
+ let k = 0;
502
+ for (let i = 0; i < results.length; i += 1) {
503
+ if (!first[i]) {
504
+ out.push(null);
505
+ continue;
506
+ }
507
+ const r = live[k];
508
+ k += 1;
509
+ out.push(settled.get(checkpointKey(r.runId, r.seq)) ?? null);
510
+ }
511
+ return out;
512
+ }
513
+ /**
514
+ * {@link settleResultsIfPending} on a dialect without `UPDATE … RETURNING` in one statement
515
+ * (MySQL, SQLite): the runs' statuses, the rows, then the settle (one conditional UPDATE for a
516
+ * single result, the batched transaction for several). The same three statements the engine's
517
+ * read-first path costs.
518
+ */
519
+ async settleResultsPlain(results) {
520
+ const out = new Map();
521
+ const client = this.client();
522
+ const runRows = (await client
523
+ .from(DURABLE_TABLES.runs)
524
+ .whereIn('id', [...new Set(results.map((r) => r.runId))])
525
+ .select('id', 'status', 'wake_at'));
526
+ const runs = new Map(runRows.map((row) => [row.id, row]));
527
+ const settleable = results.filter((r) => SETTLEABLE_RUN_STATUSES.includes(runs.get(r.runId)?.status));
528
+ if (settleable.length === 0)
529
+ return out;
530
+ // What the result does not carry keeps its stored value, so the rows are read first.
531
+ const stored = new Map();
532
+ const byRun = new Map();
533
+ for (const r of settleable)
534
+ byRun.set(r.runId, [...(byRun.get(r.runId) ?? []), r.seq]);
535
+ for (const [runId, seqs] of byRun) {
536
+ for (const cp of await this.getCheckpointsForSettle(runId, seqs)) {
537
+ stored.set(checkpointKey(cp.runId, cp.seq), cp);
538
+ }
539
+ }
540
+ const settlements = settleable.flatMap((r) => {
541
+ const cp = stored.get(checkpointKey(r.runId, r.seq));
542
+ if (!cp)
543
+ return [];
544
+ return [
545
+ {
546
+ runId: r.runId,
547
+ seq: r.seq,
548
+ status: r.status,
549
+ output: r.status === 'completed' ? r.output : cp.output,
550
+ error: r.error,
551
+ events: r.events ?? cp.events,
552
+ startedAt: r.startedAt ?? cp.startedAt,
553
+ finishedAt: r.finishedAt,
554
+ },
555
+ ];
556
+ });
557
+ const wrote = await this.settleCheckpointsIfPending(settlements);
558
+ // The rows as they now read: the stored row with the settlement written over it (the settle is a
559
+ // compare-and-set on `pending`, so nothing else wrote it in between).
560
+ settlements.forEach((settlement, i) => {
561
+ if (!wrote[i])
562
+ return;
563
+ const k = checkpointKey(settlement.runId, settlement.seq);
564
+ const { runId: _runId, seq: _seq, ...written } = settlement;
565
+ const checkpoint = { ...stored.get(k), ...written };
566
+ for (const key of ['output', 'error', 'events']) {
567
+ if (checkpoint[key] === undefined)
568
+ delete checkpoint[key];
569
+ }
570
+ const run = runs.get(settlement.runId);
571
+ out.set(k, {
572
+ checkpoint,
573
+ run: {
574
+ status: run.status,
575
+ wakeAt: run.wake_at == null ? undefined : Number(run.wake_at),
576
+ },
577
+ });
578
+ });
579
+ return out;
580
+ }
581
+ async insertCheckpointIfAbsent(checkpoint) {
582
+ const client = this.client();
583
+ const row = checkpointToRow(checkpoint);
584
+ const hot = this.hot();
585
+ if (hot) {
586
+ const keys = Object.keys(row);
587
+ const values = keys.map((k) => row[k] ?? null);
588
+ return (await hot.rows(insertCheckpointIfAbsentSql(keys), values)).length > 0;
589
+ }
590
+ if (client.dialect.name === 'postgres') {
591
+ const inserted = (await client
592
+ .insertQuery()
593
+ .table(DURABLE_TABLES.checkpoints)
594
+ .knexQuery.insert(row)
595
+ .onConflict(['run_id', 'seq'])
596
+ .ignore()
597
+ .returning('seq'));
598
+ return inserted.length > 0;
599
+ }
600
+ // MySQL and SQLite: a plain INSERT, whose duplicate-key error is the "already there" answer (an
601
+ // `INSERT IGNORE` would also swallow every other error, and reports no row count through Knex).
602
+ try {
603
+ await client.table(DURABLE_TABLES.checkpoints).insert(row);
604
+ return true;
605
+ }
606
+ catch (err) {
607
+ if (isDuplicateKey(err))
608
+ return false;
609
+ throw err;
610
+ }
611
+ }
411
612
  /** Persist the latest worker heartbeat on the step's checkpoint row (see the interface doc).
412
613
  * A targeted two-column UPDATE — never an upsert: a beat racing the checkpoint's own insert (or
413
614
  * outliving a deleted run) matches zero rows and is a silent no-op, by design. */
@@ -472,6 +673,63 @@ export class LucidStateStore {
472
673
  .limit(limit);
473
674
  return rows.map(rowToRun);
474
675
  }
676
+ async claimPendingRuns(limit, owner, leaseUntilMs, nowMs, namespace) {
677
+ if (limit <= 0)
678
+ return [];
679
+ const client = this.client();
680
+ const dialect = client.dialect.name;
681
+ const free = (q) => {
682
+ q.where('status', 'pending').andWhere((w) => {
683
+ w.whereNull('locked_until').orWhere('locked_until', '<=', nowMs);
684
+ });
685
+ if (namespace !== undefined)
686
+ q.whereIn('namespace', namespaceFilterValues(namespace));
687
+ return this.fenced(q);
688
+ };
689
+ const oldest = (q) => free(q).select('id').orderBy('created_at', 'asc').orderBy('id', 'asc').limit(limit);
690
+ let rows;
691
+ if (dialect === 'postgres') {
692
+ // One statement: pick the oldest free pending rows, skipping any another claimer holds locked
693
+ // right now, and lease them. Several pollers at once each get a different slice.
694
+ const pick = oldest(client.from(DURABLE_TABLES.runs)).forUpdate().skipLocked();
695
+ rows = (await client
696
+ .from(DURABLE_TABLES.runs)
697
+ .whereIn('id', pick)
698
+ .update({ locked_by: owner, locked_until: leaseUntilMs })
699
+ .returning('*'));
700
+ }
701
+ else if (dialect === 'mysql') {
702
+ // No UPDATE … RETURNING: lock the batch (skipping rows another claimer holds), lease it and read
703
+ // it back, in one transaction.
704
+ rows = await client.transaction(async (trx) => {
705
+ const ids = (await oldest(trx.from(DURABLE_TABLES.runs)).forUpdate().skipLocked()).map((r) => r.id);
706
+ if (ids.length === 0)
707
+ return [];
708
+ await trx
709
+ .from(DURABLE_TABLES.runs)
710
+ .whereIn('id', ids)
711
+ .update({ locked_by: owner, locked_until: leaseUntilMs });
712
+ return (await trx.from(DURABLE_TABLES.runs).whereIn('id', ids));
713
+ });
714
+ }
715
+ else {
716
+ // SQLite has no row locks to skip: each run is taken with the lease's own compare-and-set.
717
+ const claimed = [];
718
+ const candidates = (await free(client.from(DURABLE_TABLES.runs))
719
+ .orderBy('created_at', 'asc')
720
+ .orderBy('id', 'asc')
721
+ .limit(limit));
722
+ for (const run of candidates.map(rowToRun)) {
723
+ if (await this.tryLockRun(run.id, owner, leaseUntilMs, nowMs)) {
724
+ claimed.push({ ...run, lockedBy: owner, lockedUntil: leaseUntilMs });
725
+ }
726
+ }
727
+ return claimed;
728
+ }
729
+ return rows
730
+ .map(rowToRun)
731
+ .sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id));
732
+ }
475
733
  async listDueTimers(nowMs, namespace, limit) {
476
734
  const marker = await this.hasWakeMarkerColumn();
477
735
  const q = this.fenced(this.client()
@@ -506,7 +764,51 @@ export class LucidStateStore {
506
764
  .update({ locked_by: owner, locked_until: leaseUntilMs });
507
765
  return rowsAffected(affected) === 1;
508
766
  }
767
+ async tryLockRunAndGet(runId, owner, leaseUntilMs, nowMs) {
768
+ const hot = this.hot();
769
+ if (hot) {
770
+ const rows = this.scopeNamespace === undefined
771
+ ? await hot.rows(LOCK_AND_GET_SQL, [owner, leaseUntilMs, runId, nowMs])
772
+ : await hot.rows(LOCK_AND_GET_SCOPED_SQL, [
773
+ owner,
774
+ leaseUntilMs,
775
+ runId,
776
+ nowMs,
777
+ namespaceFilterValues(this.scopeNamespace),
778
+ ]);
779
+ return rows[0] ? rowToRun(rows[0]) : null;
780
+ }
781
+ const client = this.client();
782
+ if (client.dialect.name === 'postgres') {
783
+ // One statement: the lease's compare-and-set and the read of the row it locked. The scope
784
+ // fence sits in the WHERE clause, so a run outside it is neither locked nor read.
785
+ const rows = (await this.fenced(client
786
+ .from(DURABLE_TABLES.runs)
787
+ .where('id', runId)
788
+ .andWhere((q) => {
789
+ q.whereNull('locked_until').orWhere('locked_until', '<=', nowMs);
790
+ }))
791
+ .update({ locked_by: owner, locked_until: leaseUntilMs })
792
+ .returning('*'));
793
+ return rows[0] ? rowToRun(rows[0]) : null;
794
+ }
795
+ if (!(await this.tryLockRun(runId, owner, leaseUntilMs, nowMs)))
796
+ return null;
797
+ const run = await this.getRun(runId);
798
+ // Outside the store's read scope: give the lease back.
799
+ if (!run)
800
+ await this.releaseRunLock(runId, owner);
801
+ return run;
802
+ }
509
803
  async releaseRunLock(runId, owner) {
804
+ const hot = this.hot();
805
+ if (hot) {
806
+ if (owner === undefined)
807
+ await hot.run(RELEASE_LOCK_SQL, [runId]);
808
+ else
809
+ await hot.run(RELEASE_OWN_LOCK_SQL, [runId, owner]);
810
+ return;
811
+ }
510
812
  // Owner-scoped release (when `owner` is given): the `locked_by` predicate lives in the UPDATE's
511
813
  // WHERE clause, so a stale executor whose lease was taken over matches zero rows and cannot
512
814
  // wipe the new owner's lease — same conditional-write shape as renewRunLock.
@@ -519,6 +821,17 @@ export class LucidStateStore {
519
821
  // One conditional UPDATE: a marker a refused wake set after the holder's last look keeps the
520
822
  // lease where it is, and the holder runs another turn.
521
823
  const marker = await this.hasWakeMarkerColumn();
824
+ const hot = this.hot();
825
+ if (hot) {
826
+ const released = await hot.run(marker ? RELEASE_UNLESS_WAKE_SQL : RELEASE_OWN_LOCK_SQL, [
827
+ runId,
828
+ owner,
829
+ ]);
830
+ if (released.count === 1)
831
+ return true;
832
+ const rows = await hot.rows(LOCKED_BY_SQL, [runId]);
833
+ return rows[0]?.locked_by !== owner;
834
+ }
522
835
  const q = this.client()
523
836
  .from(DURABLE_TABLES.runs)
524
837
  .where('id', runId)
@@ -558,6 +871,9 @@ export class LucidStateStore {
558
871
  return this.wakeMarkerColumn;
559
872
  }
560
873
  async renewRunLock(runId, owner, leaseUntilMs) {
874
+ const hot = this.hot();
875
+ if (hot)
876
+ return (await hot.run(RENEW_LOCK_SQL, [leaseUntilMs, runId, owner])).count === 1;
561
877
  // Extend ONLY if `owner` still holds the lease — a dead worker's lease still expires & is reclaimed.
562
878
  const affected = await this.client()
563
879
  .from(DURABLE_TABLES.runs)
@@ -722,16 +1038,21 @@ export class LucidStateStore {
722
1038
  // The oldest signal is ours only if our DELETE removed it, so two takers never get the same one;
723
1039
  // that needs no transaction, and a token with nothing buffered (the common case) costs one read.
724
1040
  const client = this.client();
725
- const row = await client
726
- .from(DURABLE_TABLES.bufferedSignals)
727
- .where('token', token)
728
- .orderBy('id', 'asc')
729
- .first();
1041
+ const hot = this.hot();
1042
+ const row = hot
1043
+ ? (await hot.rows(OLDEST_BUFFERED_SIGNAL_SQL, [token]))[0]
1044
+ : await client
1045
+ .from(DURABLE_TABLES.bufferedSignals)
1046
+ .where('token', token)
1047
+ .orderBy('id', 'asc')
1048
+ .first();
730
1049
  if (!row)
731
1050
  return null;
732
1051
  const r = row;
733
- const deleted = await client.from(DURABLE_TABLES.bufferedSignals).where('id', r.id).delete();
734
- if (rowsAffected(deleted) !== 1)
1052
+ const deleted = hot
1053
+ ? (await hot.run(DELETE_BUFFERED_SIGNAL_SQL, [r.id])).count
1054
+ : rowsAffected(await client.from(DURABLE_TABLES.bufferedSignals).where('id', r.id).delete());
1055
+ if (deleted !== 1)
735
1056
  return null;
736
1057
  return {
737
1058
  payload: parseJsonColumn(r.payload, `${DURABLE_TABLES.bufferedSignals}.payload`, String(r.id)),
@@ -749,11 +1070,14 @@ export class LucidStateStore {
749
1070
  });
750
1071
  }
751
1072
  async listBufferedEvents(name, limit) {
752
- const rows = await this.client()
753
- .from(DURABLE_TABLES.bufferedEvents)
754
- .where('name', name)
755
- .orderBy('published_at', 'asc') // oldest first
756
- .limit(limit);
1073
+ const hot = this.hot();
1074
+ const rows = hot
1075
+ ? await hot.rows(BUFFERED_EVENTS_SQL, [name, limit])
1076
+ : await this.client()
1077
+ .from(DURABLE_TABLES.bufferedEvents)
1078
+ .where('name', name)
1079
+ .orderBy('published_at', 'asc') // oldest first
1080
+ .limit(limit);
757
1081
  return rows.map((r) => ({
758
1082
  id: r.id,
759
1083
  payload: parseJsonColumn(r.payload, `${DURABLE_TABLES.bufferedEvents}.payload`, r.id),
@@ -954,6 +1278,8 @@ export class LucidStateStore {
954
1278
  else
955
1279
  q.whereRaw('1 = 0');
956
1280
  }
1281
+ if (query.excludeWorkflows?.length)
1282
+ q.whereNotIn('workflow', query.excludeWorkflows);
957
1283
  if (query.namespace !== undefined) {
958
1284
  q.whereIn('namespace', namespaceFilterValues(query.namespace));
959
1285
  }
@@ -998,6 +1324,8 @@ export class LucidStateStore {
998
1324
  q.where('created_at', '>=', query.createdAfter);
999
1325
  if (query.updatedBefore !== undefined)
1000
1326
  q.where('updated_at', '<=', query.updatedBefore);
1327
+ if (query.updatedAfter !== undefined)
1328
+ q.where('updated_at', '>=', query.updatedAfter);
1001
1329
  if (query.wakeBefore !== undefined) {
1002
1330
  q.whereNotNull('wake_at').where('wake_at', '<=', query.wakeBefore);
1003
1331
  }
@@ -1052,11 +1380,14 @@ export class LucidStateStore {
1052
1380
  return rows.map(rowToCheckpoint);
1053
1381
  }
1054
1382
  async listCheckpointsForReplay(runId) {
1055
- const rows = (await this.client()
1056
- .from(DURABLE_TABLES.checkpoints)
1057
- .where('run_id', runId)
1058
- .orderBy('seq', 'asc')
1059
- .select(...CHECKPOINT_COLUMNS_BUT_INPUT));
1383
+ const hot = this.hot();
1384
+ const rows = (hot
1385
+ ? await hot.rows(REPLAY_CHECKPOINTS_SQL, [runId])
1386
+ : await this.client()
1387
+ .from(DURABLE_TABLES.checkpoints)
1388
+ .where('run_id', runId)
1389
+ .orderBy('seq', 'asc')
1390
+ .select(...CHECKPOINT_COLUMNS_BUT_INPUT));
1060
1391
  // The input of every row but a completed one, read by seq: a row that completed since the read
1061
1392
  // above still gets its input, as that read reported it unsettled.
1062
1393
  const unsettled = rows
@@ -1105,6 +1436,11 @@ export class LucidStateStore {
1105
1436
  async countCheckpoints(runId, fromSeq, toSeq, statuses) {
1106
1437
  if (statuses.length === 0)
1107
1438
  return 0;
1439
+ const hot = this.hot();
1440
+ if (hot) {
1441
+ const rows = await hot.rows(COUNT_CHECKPOINTS_SQL, [runId, fromSeq, toSeq, statuses]);
1442
+ return Number(rows[0]?.count ?? 0);
1443
+ }
1108
1444
  const rows = await this.client()
1109
1445
  .from(DURABLE_TABLES.checkpoints)
1110
1446
  .where('run_id', runId)
@@ -1324,6 +1660,113 @@ function nameKindSql(dialect) {
1324
1660
  function escapeLike(value) {
1325
1661
  return value.replace(/[!%_]/g, (c) => `!${c}`);
1326
1662
  }
1663
+ // --- the Postgres hot path's statements (see `lucid-pg-hot-path.ts`) --------------------------------
1664
+ const RUNS_T = quote(DURABLE_TABLES.runs);
1665
+ const CHECKPOINTS_T = quote(DURABLE_TABLES.checkpoints);
1666
+ const CHECKPOINT_LIST = CHECKPOINT_COLUMNS_BUT_INPUT.map(quote).join(', ');
1667
+ const GET_RUN_SQL = `select * from ${RUNS_T} where "id" = ? limit 1`;
1668
+ const GET_RUN_SCOPED_SQL = `select * from ${RUNS_T} where "id" = ? and "namespace" = any(?) limit 1`;
1669
+ const RUN_IN_STATUSES_SQL = `select 1 from ${RUNS_T} where "id" = ? and "status" = any(?) limit 1`;
1670
+ const LOCK_AND_GET_SQL = `update ${RUNS_T} set "locked_by" = ?, "locked_until" = ? where "id" = ? and ("locked_until" is null or "locked_until" <= ?) returning *`;
1671
+ const LOCK_AND_GET_SCOPED_SQL = `update ${RUNS_T} set "locked_by" = ?, "locked_until" = ? where "id" = ? and ("locked_until" is null or "locked_until" <= ?) and "namespace" = any(?) returning *`;
1672
+ const RELEASE_LOCK_SQL = `update ${RUNS_T} set "locked_by" = null, "locked_until" = null where "id" = ?`;
1673
+ const RELEASE_OWN_LOCK_SQL = `update ${RUNS_T} set "locked_by" = null, "locked_until" = null where "id" = ? and "locked_by" = ?`;
1674
+ const RELEASE_UNLESS_WAKE_SQL = `update ${RUNS_T} set "locked_by" = null, "locked_until" = null where "id" = ? and "locked_by" = ? and "wake_requested_at" is null`;
1675
+ const LOCKED_BY_SQL = `select "locked_by" from ${RUNS_T} where "id" = ? limit 1`;
1676
+ const RENEW_LOCK_SQL = `update ${RUNS_T} set "locked_until" = ? where "id" = ? and "locked_by" = ?`;
1677
+ const DELETE_ATTRIBUTES_SQL = `delete from ${quote(DURABLE_TABLES.attributes)} where "run_id" = ?`;
1678
+ const GET_CHECKPOINT_SQL = `select * from ${CHECKPOINTS_T} where "run_id" = ? and "seq" = ? limit 1`;
1679
+ const CHECKPOINTS_FOR_SETTLE_SQL = `select ${CHECKPOINT_LIST} from ${CHECKPOINTS_T} where "run_id" = ? and "seq" = any(?)`;
1680
+ const REPLAY_CHECKPOINTS_SQL = `select ${CHECKPOINT_LIST} from ${CHECKPOINTS_T} where "run_id" = ? order by "seq" asc`;
1681
+ const COUNT_CHECKPOINTS_SQL = `select count(*) as "count" from ${CHECKPOINTS_T} where "run_id" = ? and "seq" >= ? and "seq" <= ? and "status" = any(?)`;
1682
+ const OLDEST_BUFFERED_SIGNAL_SQL = `select * from ${quote(DURABLE_TABLES.bufferedSignals)} where "token" = ? order by "id" asc limit 1`;
1683
+ const DELETE_BUFFERED_SIGNAL_SQL = `delete from ${quote(DURABLE_TABLES.bufferedSignals)} where "id" = ?`;
1684
+ const BUFFERED_EVENTS_SQL = `select * from ${quote(DURABLE_TABLES.bufferedEvents)} where "name" = ? order by "published_at" asc limit ?`;
1685
+ /** Statements whose column list is the row's own keys, built once per key set. */
1686
+ const shaped = new Map();
1687
+ function shapedSql(key, build) {
1688
+ let text = shaped.get(key);
1689
+ if (text === undefined) {
1690
+ text = build();
1691
+ shaped.set(key, text);
1692
+ }
1693
+ return text;
1694
+ }
1695
+ /** `INSERT` of one row of `table` with the columns `keys`. */
1696
+ function insertSql(table, keys) {
1697
+ return shapedSql(`insert:${table}:${keys.join(',')}`, () => {
1698
+ const cols = keys.map(quote).join(', ');
1699
+ return `insert into ${quote(table)} (${cols}) values (${keys.map(() => '?').join(', ')})`;
1700
+ });
1701
+ }
1702
+ /** {@link insertSql} of a checkpoint that writes nothing when its position is taken. */
1703
+ function insertCheckpointIfAbsentSql(keys) {
1704
+ return shapedSql(`insert-if-absent:${keys.join(',')}`, () => `${insertSql(DURABLE_TABLES.checkpoints, keys)} on conflict ("run_id", "seq") do nothing returning "seq"`);
1705
+ }
1706
+ /** `UPDATE` of one run's columns `keys`, with a status predicate when `conditional`. */
1707
+ function updateRunSql(keys, conditional) {
1708
+ return shapedSql(`update-run:${conditional}:${keys.join(',')}`, () => {
1709
+ const set = keys.map((k) => `${quote(k)} = ?`).join(', ');
1710
+ const where = conditional ? `"id" = ? and "status" = any(?)` : `"id" = ?`;
1711
+ return `update ${RUNS_T} set ${set} where ${where}`;
1712
+ });
1713
+ }
1714
+ /**
1715
+ * {@link LucidStateStore.settleResultsIfPending} on Postgres: one `UPDATE … FROM (VALUES …) …
1716
+ * RETURNING` for the whole batch (entries already deduplicated by row). The run's status is checked
1717
+ * in the same statement, and read back with its `wake_at`. What a result does not carry keeps its
1718
+ * stored value: `output` unless it completed, `events` and `started_at` unless present.
1719
+ */
1720
+ async function settleResultsReturningPg(client, hot, results) {
1721
+ const values = [];
1722
+ const bindings = [];
1723
+ for (const r of results) {
1724
+ const row = settlementToRow({
1725
+ ...r,
1726
+ startedAt: r.startedAt ?? new Date(0),
1727
+ });
1728
+ values.push('(?::text, ?::int, ?::text, ?::text, ?::text, ?::boolean, ?::text, ?::bigint, ?::bigint)');
1729
+ bindings.push(r.runId, r.seq, r.status, r.status === 'completed' ? row.output : null, row.error, r.events !== undefined, row.events, r.startedAt ? row.started_at : null, row.finished_at);
1730
+ }
1731
+ const cols = CHECKPOINT_COLUMNS_BUT_INPUT.map((c) => `c.${c}`).join(', ');
1732
+ const statuses = SETTLEABLE_RUN_STATUSES.map(() => '?').join(', ');
1733
+ bindings.push(...SETTLEABLE_RUN_STATUSES);
1734
+ const sql = `update ${DURABLE_TABLES.checkpoints} as c set
1735
+ status = v.status,
1736
+ output = case when v.status = 'completed' then v.output else c.output end,
1737
+ error = v.error,
1738
+ events = case when v.has_events then v.events else c.events end,
1739
+ started_at = coalesce(v.started_at, c.started_at),
1740
+ finished_at = v.finished_at
1741
+ from (values ${values.join(', ')})
1742
+ as v(run_id, seq, status, output, error, has_events, events, started_at, finished_at),
1743
+ ${DURABLE_TABLES.runs} as r
1744
+ where c.run_id = v.run_id and c.seq = v.seq and c.status = 'pending'
1745
+ and r.id = c.run_id and r.status in (${statuses})
1746
+ returning ${cols}, r.status as run_status, r.wake_at as run_wake_at`;
1747
+ const { rows } = (hot ? { rows: await hot.rows(sql, bindings) } : await client.rawQuery(sql, bindings));
1748
+ const out = new Map();
1749
+ for (const row of rows) {
1750
+ const checkpoint = rowToCheckpoint(row);
1751
+ out.set(checkpointKey(checkpoint.runId, checkpoint.seq), {
1752
+ checkpoint,
1753
+ run: {
1754
+ status: row.run_status,
1755
+ wakeAt: row.run_wake_at == null ? undefined : Number(row.run_wake_at),
1756
+ },
1757
+ });
1758
+ }
1759
+ return out;
1760
+ }
1761
+ /** A duplicate-key violation, as MySQL and SQLite report it through Knex. */
1762
+ function isDuplicateKey(err) {
1763
+ const e = err;
1764
+ return (e?.code === 'ER_DUP_ENTRY' ||
1765
+ e?.errno === 1062 ||
1766
+ e?.code === 'SQLITE_CONSTRAINT_PRIMARYKEY' ||
1767
+ e?.code === 'SQLITE_CONSTRAINT_UNIQUE' ||
1768
+ e?.code === '23505');
1769
+ }
1327
1770
  function checkpointKey(runId, seq) {
1328
1771
  return `${runId}\u0000${seq}`;
1329
1772
  }