@lostgradient/weft 0.24.0 → 0.24.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.
Files changed (243) hide show
  1. package/README.md +2 -2
  2. package/dist/cli/conformance.js +5 -2
  3. package/dist/cli/generated/operation-catalog.snapshot.json +95 -1
  4. package/dist/cli/generated/operation-client.generated.d.ts +28 -2
  5. package/dist/cli/generated/operation-client.generated.js +2 -0
  6. package/dist/cli/parse-schedule-arguments.js +15 -3
  7. package/dist/cli/schedule.js +2 -1
  8. package/dist/cli/types.d.ts +2 -1
  9. package/dist/client/http-client-requests.js +6 -9
  10. package/dist/client/start-body.js +1 -0
  11. package/dist/core/catalog/index.d.ts +2 -2
  12. package/dist/core/catalog/index.js +9 -2
  13. package/dist/core/catalog/reference-counts.d.ts +29 -14
  14. package/dist/core/catalog/removal.d.ts +63 -10
  15. package/dist/core/catalog/removal.js +18 -2
  16. package/dist/core/catalog/storage-io.d.ts +23 -0
  17. package/dist/core/catalog/storage-io.js +1 -1
  18. package/dist/core/engine/activity-resolution.js +17 -17
  19. package/dist/core/engine/bulk-operations-purge.js +14 -20
  20. package/dist/core/engine/bulk-operations-retry.d.ts +9 -0
  21. package/dist/core/engine/bulk-operations-retry.js +177 -0
  22. package/dist/core/engine/bulk-operations-shared.d.ts +18 -0
  23. package/dist/core/engine/bulk-operations-shared.js +14 -0
  24. package/dist/core/engine/bulk-operations.js +8 -167
  25. package/dist/core/engine/callback-creators-bundles.js +4 -2
  26. package/dist/core/engine/callback-creators-core.js +9 -1
  27. package/dist/core/engine/callback-creators-schedule.js +2 -2
  28. package/dist/core/engine/catalog-readiness.js +2 -0
  29. package/dist/core/engine/catalog-removal.d.ts +107 -3
  30. package/dist/core/engine/catalog-removal.js +66 -13
  31. package/dist/core/engine/catalog-tombstone-recovery.d.ts +56 -0
  32. package/dist/core/engine/catalog-tombstone-recovery.js +39 -0
  33. package/dist/core/engine/constraints.js +5 -1
  34. package/dist/core/engine/construction.d.ts +1 -1
  35. package/dist/core/engine/decode-revision.d.ts +32 -0
  36. package/dist/core/engine/decode-revision.js +9 -0
  37. package/dist/core/engine/disposal.d.ts +6 -2
  38. package/dist/core/engine/disposal.js +24 -10
  39. package/dist/core/engine/dynamic-source-errors.d.ts +81 -0
  40. package/dist/core/engine/dynamic-source-errors.js +25 -0
  41. package/dist/core/engine/dynamic-source-execution.d.ts +173 -0
  42. package/dist/core/engine/dynamic-source-execution.js +108 -0
  43. package/dist/core/engine/engine-internal-types.d.ts +16 -0
  44. package/dist/core/engine/engine-workflows-namespace.d.ts +10 -0
  45. package/dist/core/engine/engine-workflows-namespace.js +3 -1
  46. package/dist/core/engine/index.d.ts +18 -10
  47. package/dist/core/engine/index.js +21 -10
  48. package/dist/core/engine/inline-launch-queue.js +1 -1
  49. package/dist/core/engine/internals.d.ts +26 -30
  50. package/dist/core/engine/lifecycle/checkpoint-launch.d.ts +29 -0
  51. package/dist/core/engine/lifecycle/checkpoint-launch.js +75 -0
  52. package/dist/core/engine/lifecycle/delayed-start-registration.d.ts +30 -0
  53. package/dist/core/engine/lifecycle/delayed-start-registration.js +11 -0
  54. package/dist/core/engine/lifecycle/fork-helpers.d.ts +33 -1
  55. package/dist/core/engine/lifecycle/fork-helpers.js +3 -1
  56. package/dist/core/engine/lifecycle/recovery-revision-groups.d.ts +75 -0
  57. package/dist/core/engine/lifecycle/recovery-revision-groups.js +64 -0
  58. package/dist/core/engine/lifecycle/resume-generation-guard.d.ts +58 -0
  59. package/dist/core/engine/lifecycle/resume-generation-guard.js +11 -0
  60. package/dist/core/engine/lifecycle/resume.js +18 -9
  61. package/dist/core/engine/lifecycle/shared.d.ts +43 -0
  62. package/dist/core/engine/lifecycle/standalone-claim-acquire.d.ts +12 -0
  63. package/dist/core/engine/lifecycle/standalone-claim-acquire.js +6 -0
  64. package/dist/core/engine/lifecycle/start-commit-errors.d.ts +12 -0
  65. package/dist/core/engine/lifecycle/start-commit-errors.js +6 -0
  66. package/dist/core/engine/lifecycle/start-commit.d.ts +22 -7
  67. package/dist/core/engine/lifecycle/start-commit.js +49 -15
  68. package/dist/core/engine/lifecycle/start-exec.d.ts +18 -2
  69. package/dist/core/engine/lifecycle/start-exec.js +7 -5
  70. package/dist/core/engine/lifecycle/start-or-signal-create.js +1 -3
  71. package/dist/core/engine/lifecycle/start-or-signal.js +1 -1
  72. package/dist/core/engine/lifecycle/start-precondition-attribution.d.ts +49 -0
  73. package/dist/core/engine/lifecycle/start-precondition-attribution.js +13 -0
  74. package/dist/core/engine/lifecycle/start-revision-resolution.d.ts +34 -0
  75. package/dist/core/engine/lifecycle/start-revision-resolution.js +11 -0
  76. package/dist/core/engine/lifecycle/start-state.d.ts +16 -0
  77. package/dist/core/engine/lifecycle/start-state.js +60 -0
  78. package/dist/core/engine/lifecycle/start-terminal-conflict-purge.d.ts +66 -3
  79. package/dist/core/engine/lifecycle/start-terminal-conflict-purge.js +13 -3
  80. package/dist/core/engine/lifecycle/start.d.ts +16 -6
  81. package/dist/core/engine/lifecycle/start.js +38 -68
  82. package/dist/core/engine/lifecycle/transition.d.ts +3 -4
  83. package/dist/core/engine/lifecycle/transition.js +30 -80
  84. package/dist/core/engine/lifecycle.d.ts +4 -2
  85. package/dist/core/engine/lifecycle.js +6 -6
  86. package/dist/core/engine/listing.js +15 -1
  87. package/dist/core/engine/memo-durable-activity.js +4 -1
  88. package/dist/core/engine/nonterminal-revision-count.d.ts +25 -0
  89. package/dist/core/engine/nonterminal-revision-count.js +16 -0
  90. package/dist/core/engine/operations-time.d.ts +5 -23
  91. package/dist/core/engine/operations-time.js +13 -11
  92. package/dist/core/engine/ownership-bootstrap.d.ts +1 -1
  93. package/dist/core/engine/pinned-schedule-revision-count.d.ts +26 -0
  94. package/dist/core/engine/pinned-schedule-revision-count.js +19 -0
  95. package/dist/core/engine/pinned-schedule-revision.d.ts +76 -0
  96. package/dist/core/engine/pinned-schedule-revision.js +53 -0
  97. package/dist/core/engine/registration.d.ts +30 -0
  98. package/dist/core/engine/registration.js +17 -2
  99. package/dist/core/engine/retention.js +9 -2
  100. package/dist/core/engine/revision-errors.d.ts +71 -0
  101. package/dist/core/engine/revision-errors.js +13 -0
  102. package/dist/core/engine/schedule-revision-fire.d.ts +17 -0
  103. package/dist/core/engine/schedule-revision-fire.js +3 -0
  104. package/dist/core/engine/schedule-run.js +2 -1
  105. package/dist/core/engine/schedules.d.ts +2 -1
  106. package/dist/core/engine/schedules.js +28 -9
  107. package/dist/core/engine/source-diagnostics.d.ts +76 -0
  108. package/dist/core/engine/source-diagnostics.js +105 -0
  109. package/dist/core/engine/source-registration.js +4 -2
  110. package/dist/core/engine/source-resolution.d.ts +25 -9
  111. package/dist/core/engine/source-resolution.js +49 -14
  112. package/dist/core/engine/source-runtime-state.d.ts +92 -0
  113. package/dist/core/engine/source-runtime-state.js +36 -0
  114. package/dist/core/engine/storage-io.d.ts +19 -2
  115. package/dist/core/engine/storage-io.js +21 -2
  116. package/dist/core/engine/termination/finalizer-registration.d.ts +18 -0
  117. package/dist/core/engine/termination/finalizer-registration.js +21 -0
  118. package/dist/core/engine/termination/finalizer.d.ts +16 -19
  119. package/dist/core/engine/termination/finalizer.js +3 -3
  120. package/dist/core/engine/time-operation-callbacks.d.ts +32 -0
  121. package/dist/core/engine/time-operation-callbacks.js +0 -0
  122. package/dist/core/engine/validation/schedule-cadence.d.ts +16 -0
  123. package/dist/core/engine/validation/schedule-cadence.js +20 -0
  124. package/dist/core/engine/validation/schedule-options.d.ts +4 -1
  125. package/dist/core/engine/validation/schedule-options.js +14 -0
  126. package/dist/core/engine/validation/schedule-revision.d.ts +24 -0
  127. package/dist/core/engine/validation/schedule-revision.js +21 -0
  128. package/dist/core/engine/validation/schedule-warnings.d.ts +11 -0
  129. package/dist/core/engine/validation/schedule-warnings.js +5 -0
  130. package/dist/core/engine/validation/schedule.d.ts +4 -3
  131. package/dist/core/engine/validation/schedule.js +15 -26
  132. package/dist/core/engine/validation.js +3 -0
  133. package/dist/core/engine/workflow-claim-reclaim-target.d.ts +1 -1
  134. package/dist/core/engine/workflow-claim-reclaim-target.js +16 -2
  135. package/dist/core/engine/workflow-retention-deadline.d.ts +16 -0
  136. package/dist/core/engine/workflow-retention-deadline.js +33 -0
  137. package/dist/core/events/event-map.d.ts +5 -0
  138. package/dist/core/events/index.d.ts +1 -0
  139. package/dist/core/events/index.js +1 -0
  140. package/dist/core/events/workflow-source-events.d.ts +102 -0
  141. package/dist/core/events/workflow-source-events.js +57 -0
  142. package/dist/core/execution-strategy.d.ts +7 -0
  143. package/dist/core/inline-execution-strategy.context-options.d.ts +1 -1
  144. package/dist/core/inline-execution-strategy.js +1 -1
  145. package/dist/core/types/checkpoint.d.ts +14 -0
  146. package/dist/core/types/schedules.d.ts +52 -1
  147. package/dist/core/types/state.d.ts +16 -0
  148. package/dist/core/weft-error.d.ts +1 -1
  149. package/dist/core/weft-error.js +4 -1
  150. package/dist/core/worker-execution-strategy.d.ts +1 -0
  151. package/dist/core/worker-execution-strategy.js +13 -1
  152. package/dist/core/worker-fault-handling.d.ts +10 -0
  153. package/dist/core/worker-fault-handling.js +1 -0
  154. package/dist/core/worker-inbound-message.d.ts +1 -0
  155. package/dist/core/worker-inbound-message.js +1 -0
  156. package/dist/core/worker-protocol-guard.js +4 -0
  157. package/dist/core/worker-protocol.d.ts +7 -0
  158. package/dist/core/worker-protocol.js +2 -1
  159. package/dist/core/worker-turn-watchdog.d.ts +8 -1
  160. package/dist/core/worker-turn-watchdog.js +3 -2
  161. package/dist/diagnostics/format.js +5 -0
  162. package/dist/diagnostics/types.d.ts +24 -0
  163. package/dist/diagnostics/version-check.js +8 -2
  164. package/dist/http.js +1 -1
  165. package/dist/index.d.ts +3 -3
  166. package/dist/index.js +7 -0
  167. package/dist/indexeddb.js +1 -1
  168. package/dist/json-schema.js +3 -3
  169. package/dist/server/fault-to-json-rpc.js +2 -1
  170. package/dist/server/index.d.ts +11 -0
  171. package/dist/server/operation-fault.d.ts +2 -0
  172. package/dist/server/operation-fault.js +2 -1
  173. package/dist/server/operations/create-schedule.d.ts +2 -0
  174. package/dist/server/operations/create-schedule.js +10 -6
  175. package/dist/server/operations/get-catalog-diagnostics.d.ts +28 -0
  176. package/dist/server/operations/get-catalog-diagnostics.js +10 -2
  177. package/dist/server/operations/get-task-detail-schema.d.ts +8 -0
  178. package/dist/server/operations/get-task-detail-schema.js +1 -0
  179. package/dist/server/operations/get-task-detail.d.ts +8 -0
  180. package/dist/server/operations/get-task-detail.js +1 -0
  181. package/dist/server/operations/get-task-diagnostics.d.ts +4 -4
  182. package/dist/server/operations/preload-workflow-revision.d.ts +28 -0
  183. package/dist/server/operations/preload-workflow-revision.js +60 -0
  184. package/dist/server/operations/schedule-faults.d.ts +1 -0
  185. package/dist/server/operations/schedule-faults.js +32 -4
  186. package/dist/server/operations/schedule-rest-body.d.ts +1 -0
  187. package/dist/server/operations/schedule-rest-body.js +2 -1
  188. package/dist/server/operations/static-registrations.js +6 -0
  189. package/dist/server/operations/update-schedule.d.ts +2 -0
  190. package/dist/server/operations/update-schedule.js +4 -2
  191. package/dist/server/operations/workflow-catalog-operation-helpers.js +27 -0
  192. package/dist/server/runtime/task-dispatch-envelope.d.ts +14 -0
  193. package/dist/server/runtime/task-dispatch-envelope.js +32 -0
  194. package/dist/server/runtime/task-dispatch-revision.d.ts +29 -0
  195. package/dist/server/runtime/task-dispatch-revision.js +13 -0
  196. package/dist/server/runtime/task-dispatch.js +12 -37
  197. package/dist/server/runtime/task-ledger-recovery.js +1 -1
  198. package/dist/server/runtime/task-ledger-runtime.d.ts +14 -2
  199. package/dist/server/runtime/task-ledger-runtime.js +5 -3
  200. package/dist/server/runtime/task-polling.js +9 -3
  201. package/dist/server/runtime/task-reconciliation.js +2 -1
  202. package/dist/server/runtime/websocket-worker.js +9 -0
  203. package/dist/server/task-ledger-codec.d.ts +9 -0
  204. package/dist/server/task-ledger-codec.js +4 -1
  205. package/dist/server/task-ledger-transition-helpers.js +1 -0
  206. package/dist/server/task-ledger-types.d.ts +9 -0
  207. package/dist/server/task-ledger.d.ts +1 -1
  208. package/dist/server/task-ledger.js +2 -1
  209. package/dist/server/task-queue-types.d.ts +2 -0
  210. package/dist/storage/bun-sql.js +3 -1
  211. package/dist/storage/catalog-keys.d.ts +19 -0
  212. package/dist/storage/catalog-keys.js +3 -1
  213. package/dist/storage/compressed-storage.js +1 -1
  214. package/dist/storage/index.d.ts +3 -1
  215. package/dist/storage/interface.d.ts +2 -0
  216. package/dist/storage/interface.js +1 -1
  217. package/dist/storage/key-prefixes.d.ts +1 -1
  218. package/dist/storage/key-prefixes.js +1 -0
  219. package/dist/storage/lmdb.d.ts +30 -2
  220. package/dist/storage/lmdb.js +1 -1
  221. package/dist/storage/memory.js +1 -1
  222. package/dist/storage/neon.js +2 -2
  223. package/dist/storage/node-sqlite.js +3 -1
  224. package/dist/storage/postgres.js +2 -2
  225. package/dist/storage/resolve.js +1 -1
  226. package/dist/storage/scoped-storage.js +1 -1
  227. package/dist/storage/testing.js +1 -1
  228. package/dist/storage/turso.js +1 -1
  229. package/dist/version.d.ts +1 -1
  230. package/dist/version.js +1 -1
  231. package/dist/web-extension.js +1 -1
  232. package/dist/worker/protocol-messages.d.ts +17 -0
  233. package/dist/worker/protocol-schemas.d.ts +32 -0
  234. package/dist/worker/protocol-schemas.js +7 -3
  235. package/dist/worker/protocol-task-result.js +35 -3
  236. package/dist/worker/protocol.js +1 -1
  237. package/dist/worker/registry/types.d.ts +2 -0
  238. package/dist/worker/registry.d.ts +1 -1
  239. package/dist/worker/registry.js +3 -1
  240. package/dist/workers/workflow-runner.d.ts +9 -0
  241. package/dist/workers/workflow-runner.js +9 -2
  242. package/dist/workers/workflow-worker-entry.js +9 -4
  243. package/package.json +2 -2
@@ -1,15 +1,8 @@
1
- import {
2
- KEYS,
3
- requireStorageCapability,
4
- storageHas
5
- } from "../../storage/interface.js";
1
+ import { KEYS, storageHas } from "../../storage/interface.js";
6
2
  import { assertScopedBulkWorkflowFilter } from "../bulk-workflow-filter.js";
7
- import { deserializeCheckpoint } from "../checkpoint.js";
8
- import { decode, encode } from "../codec.js";
9
- import { buildTimerBatchOperations } from "../scheduler.js";
10
- import { buildIndexOperations } from "../search-attributes.js";
11
- import { buildTerminalWorkflowIndexOperations, bulkMutateWorkflowTags } from "./attributes-tags.js";
3
+ import { bulkMutateWorkflowTags } from "./attributes-tags.js";
12
4
  import { purgeInternal, purgeWorkflow } from "./bulk-operations-purge.js";
5
+ import { runBulkFailedWorkflowRetry } from "./bulk-operations-retry.js";
13
6
  import {
14
7
  buildActionableBulkWorkflowFilter,
15
8
  buildBulkOperationPreparation,
@@ -17,43 +10,19 @@ import {
17
10
  normalizeBulkOperationOptions,
18
11
  prepareBulkOperation,
19
12
  resolveBulkOperationConcurrency,
13
+ runBulkWorkflowPool,
20
14
  shouldPersistBulkAudit,
21
15
  toBulkOperationError,
22
16
  validateBulkConfirmation,
23
17
  withBulkAuditEvent
24
18
  } from "./bulk-operations-shared.js";
25
19
  import { BulkDeleteRequiresTerminalWorkflowsError } from "./errors.js";
26
- import {
27
- assertLeaseHeldForEngineWork,
28
- commitFencedEngineWriteAllowingPreconditionFailure
29
- } from "./fenced-write.js";
20
+ import { assertLeaseHeldForEngineWork } from "./fenced-write.js";
30
21
  import { BULK_OPERATION_BATCH_SIZE } from "./listing.js";
31
- import { createTerminalCleanupTimerId } from "./state-utilities.js";
32
- import { loadWorkflowState, runSerializedWorkflowStateWrite } from "./storage-io.js";
33
- import { decodeWorkflowState, isTerminalWorkflowStatus } from "./validation.js";
34
- import {
35
- commitWithWorkflowClaimFold,
36
- prepareWorkflowClaimFold,
37
- throwWorkflowClaimUnavailable
38
- } from "./workflow-claim-fold.js";
39
- import { buildWorkflowConcurrencyStartOperations } from "./workflow-concurrency.js";
40
- import { buildWorkflowVisibilityIndexTransition } from "./workflow-indexes.js";
22
+ import { loadWorkflowState } from "./storage-io.js";
23
+ import { isTerminalWorkflowStatus } from "./validation.js";
41
24
  export { purgeInternal, TERMINAL_CLEANUP_DELAY_MS } from "./bulk-operations-purge.js";
42
- const ACTIVE_WORKFLOW_STATUSES = ["pending", "running", "suspended"], FAILED_WORKFLOW_STATUSES = ["failed"], CHECKPOINT_RETRY_CONCURRENCY_ADMISSION_MAX_ATTEMPTS = 5;
43
- async function runBulkWorkflowPool(items, concurrencyLimit, operation) {
44
- const results = [];
45
- for (let batchStart = 0;batchStart < items.length; batchStart += concurrencyLimit) {
46
- const batchItems = items.slice(batchStart, batchStart + concurrencyLimit);
47
- results.push(...await Promise.all(batchItems.map(async (item) => {
48
- try {
49
- return { item, status: "fulfilled", value: await operation(item) };
50
- } catch (reason) {
51
- return { item, status: "rejected", reason };
52
- }
53
- })));
54
- }
55
- return results;
56
- }
25
+ const ACTIVE_WORKFLOW_STATUSES = ["pending", "running", "suspended"];
57
26
  export async function purge(internals, filter, cleanupWaiters) {
58
27
  return purgeInternal(internals, filter, { expiredOnly: !1, now: internals.options.getNow() }, cleanupWaiters);
59
28
  }
@@ -95,138 +64,10 @@ async function runBulkCancellation(internals, filter, options = {}) {
95
64
  export async function cancelAll(internals, filter, options = {}) {
96
65
  return runBulkCancellation(internals, filter, options);
97
66
  }
98
- async function runBulkFailedWorkflowRetry(internals, filter, options = {}) {
99
- options = normalizeBulkOperationOptions(options);
100
- assertScopedBulkWorkflowFilter(filter);
101
- const actionableFilter = buildActionableBulkWorkflowFilter(internals, filter, FAILED_WORKFLOW_STATUSES), preparation = await prepareBulkOperation(internals, "retry-failed", actionableFilter, filter, {}, options);
102
- if (options.dryRun === !0)
103
- return preparation.preview;
104
- validateBulkConfirmation(options, preparation);
105
- const bulkConcurrency = resolveBulkOperationConcurrency(options);
106
- let retried = 0;
107
- const errors = [], retryResults = await runBulkWorkflowPool(preparation.workflowIds, bulkConcurrency, async (workflowId) => {
108
- await retryFailedWorkflow(internals, workflowId);
109
- return { status: "retried" };
110
- });
111
- for (const retryResult of retryResults) {
112
- if (retryResult.status === "rejected") {
113
- errors.push(toBulkOperationError(internals, retryResult.item, retryResult.reason));
114
- continue;
115
- }
116
- retried += 1;
117
- }
118
- const result = { retried, failed: errors.length, errors };
119
- if (!shouldPersistBulkAudit(options))
120
- return result;
121
- return withBulkAuditEvent(internals, preparation, options, result, retried);
122
- }
123
67
  export async function retryFailedAll(internals, filter, options = {}) {
124
68
  assertLeaseHeldForEngineWork(internals);
125
69
  return runBulkFailedWorkflowRetry(internals, filter, options);
126
70
  }
127
- async function retryFailedWorkflow(internals, workflowId) {
128
- const state = await loadWorkflowState(internals, workflowId);
129
- if (state === null)
130
- throw Error("Workflow no longer exists");
131
- if (state.status !== "failed")
132
- throw Error(`Workflow is ${state.status}, not failed`);
133
- if (await internals.storage.get(KEYS.checkpoint(workflowId)) !== null) {
134
- await reactivateFailedWorkflowFromCheckpoint(internals, state);
135
- await internals.engine.resume(workflowId);
136
- return;
137
- }
138
- await internals.engine.start(state.type, state.input, {
139
- id: workflowId,
140
- onTerminalConflict: "start-new",
141
- ...state.tags !== void 0 ? { tags: state.tags } : {}
142
- });
143
- }
144
- async function reactivateFailedWorkflowFromCheckpoint(internals, state) {
145
- const reactivated = await runSerializedWorkflowStateWrite(internals, state.id, async () => reactivateFailedWorkflowFromCheckpointSerialized(internals, state.id));
146
- if (reactivated.terminalCleanupTimerId !== void 0)
147
- await internals.scheduler.cancel(reactivated.terminalCleanupTimerId, state.id);
148
- }
149
- async function reactivateFailedWorkflowFromCheckpointSerialized(internals, workflowId) {
150
- let lastConcurrencyStateKey;
151
- for (let attempt = 0;attempt < CHECKPOINT_RETRY_CONCURRENCY_ADMISSION_MAX_ATTEMPTS; attempt += 1) {
152
- const currentStateBytes = await internals.storage.get(KEYS.workflow(workflowId));
153
- if (currentStateBytes === null)
154
- throw Error("Workflow no longer exists");
155
- const currentState = decodeWorkflowState(currentStateBytes);
156
- if (currentState.status !== "failed")
157
- throw Error(`Workflow is ${currentState.status}, not failed`);
158
- const currentCheckpointBytes = await internals.storage.get(KEYS.checkpoint(workflowId));
159
- if (currentCheckpointBytes === null)
160
- throw Error("Checkpoint no longer exists");
161
- const checkpoint = deserializeCheckpoint(currentCheckpointBytes), registration = internals.registrations.get(currentState.type);
162
- if (registration === void 0)
163
- throw Error(`No workflow registered with name "${currentState.type}" (needed to retry "${workflowId}")`);
164
- const concurrencyStartOperations = registration.concurrency === void 0 ? void 0 : await buildWorkflowConcurrencyStartOperations(internals, currentState.type, workflowId, currentState.input, registration.concurrency);
165
- lastConcurrencyStateKey = concurrencyStartOperations?.stateKey ?? lastConcurrencyStateKey;
166
- const reactivatedState = buildReactivatedWorkflowState(internals, currentState), currentAttributes = await loadSearchAttributes(internals, workflowId), operations = [
167
- ...buildTerminalWorkflowIndexOperations(currentState, reactivatedState),
168
- { type: "put", key: KEYS.workflow(workflowId), value: encode(reactivatedState) },
169
- ...buildWorkflowVisibilityIndexTransition(workflowId, currentState, reactivatedState).batchOps,
170
- ...buildRetrySearchAttributeOperations(workflowId, currentAttributes, checkpoint.searchAttributes),
171
- ...reactivatedState.executionDeadline === void 0 ? [] : buildTimerBatchOperations({
172
- id: `deadline:${workflowId}`,
173
- workflowId,
174
- fireAt: reactivatedState.executionDeadline,
175
- kind: "execution-deadline"
176
- }),
177
- ...concurrencyStartOperations?.operations ?? []
178
- ], conditions = concurrencyStartOperations?.conditions ?? [];
179
- if (await commitFailedWorkflowReactivation(internals, workflowId, operations, conditions))
180
- return {
181
- terminalCleanupTimerId: currentState.terminalCleanupToken === void 0 ? void 0 : createTerminalCleanupTimerId(!1, currentState.terminalCleanupToken)
182
- };
183
- }
184
- throw Error(`Workflow concurrency admission for "${lastConcurrencyStateKey ?? workflowId}" changed too many times while retrying failed workflow "${workflowId}"`);
185
- }
186
- async function commitFailedWorkflowReactivation(internals, workflowId, operations, conditions) {
187
- const claimFold = await prepareWorkflowClaimFold(internals, workflowId);
188
- if (claimFold === void 0) {
189
- if (conditions.length > 0)
190
- requireStorageCapability(internals.storage, "conditionalBatch", "retry failed workflow");
191
- return commitFencedEngineWriteAllowingPreconditionFailure(internals, workflowId, operations, conditions);
192
- }
193
- const result = await commitWithWorkflowClaimFold(internals, claimFold, operations, conditions, "retry failed workflow claim acquisition");
194
- if (result.status === "committed")
195
- return !0;
196
- return result.claimConflict ? throwWorkflowClaimUnavailable(internals, workflowId) : !1;
197
- }
198
- function buildReactivatedWorkflowState(internals, state) {
199
- const reactivatedState = {
200
- ...state,
201
- status: "running",
202
- updatedAt: internals.options.getNow()
203
- };
204
- delete reactivatedState.error;
205
- delete reactivatedState.errorStack;
206
- delete reactivatedState.failureCategory;
207
- delete reactivatedState.result;
208
- delete reactivatedState.terminationReason;
209
- delete reactivatedState.terminalCleanupToken;
210
- return reactivatedState;
211
- }
212
- async function loadSearchAttributes(internals, workflowId) {
213
- const attributeBytes = await internals.storage.get(KEYS.attribute(workflowId));
214
- if (attributeBytes === null)
215
- return {};
216
- return decode(attributeBytes);
217
- }
218
- function buildRetrySearchAttributeOperations(workflowId, currentAttributes, checkpointAttributes) {
219
- const operations = buildIndexOperations(workflowId, currentAttributes, checkpointAttributes);
220
- if (Object.keys(checkpointAttributes).length === 0)
221
- operations.push({ type: "delete", key: KEYS.attribute(workflowId) });
222
- else
223
- operations.push({
224
- type: "put",
225
- key: KEYS.attribute(workflowId),
226
- value: encode(checkpointAttributes)
227
- });
228
- return operations;
229
- }
230
71
  export async function signalAll(internals, filter, name, payload, maybeOptions) {
231
72
  const options = normalizeBulkOperationOptions(maybeOptions ?? {});
232
73
  assertScopedBulkWorkflowFilter(filter);
@@ -16,6 +16,7 @@ import {
16
16
  createReviewOperationCallbacks,
17
17
  handleScheduleTimerForEngine
18
18
  } from "./callback-creators-schedule.js";
19
+ import { resolveExecutableRegistrationForRevision } from "./dynamic-source-execution.js";
19
20
  import { commitFencedEngineWrite } from "./fenced-write.js";
20
21
  import { guardTerminalWorkflow, guardTerminalWorkflowAfterCoordinatedRequest } from "./guards.js";
21
22
  import { getInternals } from "./internals.js";
@@ -122,7 +123,7 @@ export function createTimeOperationCallbacks(engine) {
122
123
  loadWorkflowState: (workflowId) => loadWorkflowState(getInternals(engine), workflowId),
123
124
  failWorkflow: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine)),
124
125
  runSerializedWorkflowStateWrite: (workflowId, writeOperation) => runSerializedWorkflowStateWrite(getInternals(engine), workflowId, writeOperation),
125
- beginWorkflowExecution: (workflowId, workflowExecutionToken, workflowType, input, checkpoint, executionDeadline, executionStateOwnerId, registration) => beginWorkflowExecution(getInternals(engine), workflowId, workflowExecutionToken, workflowType, input, checkpoint, executionDeadline, executionStateOwnerId, registration, createLifecycleCallbacks(engine)),
126
+ beginWorkflowExecution: (workflowId, workflowExecutionToken, workflowType, revision, input, checkpoint, executionDeadline, executionStateOwnerId, registration) => beginWorkflowExecution(getInternals(engine), workflowId, workflowExecutionToken, workflowType, revision, input, checkpoint, executionDeadline, executionStateOwnerId, registration, createLifecycleCallbacks(engine)),
126
127
  workflowVersionTupleFromState: (state) => workflowVersionTupleFromState(getInternals(engine), state, createLifecycleCallbacks(engine)),
127
128
  setWorkflowStartHeaders: (workflowId, headers) => setWorkflowStartHeaders(getInternals(engine), workflowId, headers, createLifecycleCallbacks(engine)),
128
129
  loadWorkflowStartHeaders: (workflowId) => loadWorkflowStartHeaders(getInternals(engine), workflowId, createLifecycleCallbacks(engine)),
@@ -131,7 +132,8 @@ export function createTimeOperationCallbacks(engine) {
131
132
  runWorkflowFinalizer: (workflowId, timerId) => runWorkflowFinalizer(getInternals(engine), workflowId, timerId, createTerminationCallbacks(engine)),
132
133
  handleScheduleTimer: (entry) => handleScheduleTimerForEngine(engine, entry),
133
134
  timeout: (workflowId) => engine.timeout(workflowId),
134
- handleCleanupError: (source, error, workflowId) => createTerminationCallbacks(engine).handleCleanupError(source, error, workflowId)
135
+ handleCleanupError: (source, error, workflowId) => createTerminationCallbacks(engine).handleCleanupError(source, error, workflowId),
136
+ resolveExecutableRegistrationForRevision: (type, revision) => resolveExecutableRegistrationForRevision(engine, getInternals(engine), type, revision)
135
137
  };
136
138
  }
137
139
  export function createUpdateCallbacks(engine) {
@@ -3,6 +3,10 @@ import {
3
3
  broadcast as broadcastFromInternals,
4
4
  forwardEventToHandle as forwardEventToHandleFromBroadcast
5
5
  } from "./broadcast.js";
6
+ import {
7
+ resolveExecutableRegistration,
8
+ resolveExecutableRegistrationForRevision
9
+ } from "./dynamic-source-execution.js";
6
10
  import { createWorkflowHandleWithResultPromise } from "./handle-result.js";
7
11
  import {
8
12
  hasLocalCheckpointOwnership,
@@ -79,7 +83,11 @@ export function createLifecycleCallbacks(engine) {
79
83
  failWorkflowForUnavailableServices: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine), "system"),
80
84
  failWorkflowForRecoveryHook: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine), "system"),
81
85
  failWorkflowForCheckpointDecodeError: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine), "system"),
82
- failWorkflowForVersionMismatch: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine), "system")
86
+ failWorkflowForVersionMismatch: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine), "system"),
87
+ resolveExecutableRegistration: (type, onRevisionChosen) => resolveExecutableRegistration(engine, getInternals(engine), type, onRevisionChosen),
88
+ failWorkflowForUnavailableDynamicSource: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine), "system"),
89
+ resolveExecutableRegistrationForRevision: (type, revision) => resolveExecutableRegistrationForRevision(engine, getInternals(engine), type, revision),
90
+ failWorkflowForRevisionUnavailable: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine), "system")
83
91
  };
84
92
  }
85
93
  export function createTerminationCallbacksWith(engine, handleScheduledWorkflowTerminal) {
@@ -20,8 +20,8 @@ import { feedOperationResult } from "./strategy-helpers.js";
20
20
  import { ensureTerminalCleanupTracked, failWorkflow, handleCleanupError } from "./termination.js";
21
21
  export function createScheduleCallbacks(engine) {
22
22
  return {
23
- startWorkflow: async (type, input, options, additionalStartOperations) => {
24
- await startWorkflow(getInternals(engine), type, input, options, additionalStartOperations, createLifecycleCallbacks(engine));
23
+ startWorkflow: async (type, input, options, additionalStartOperations, revisionOverride) => {
24
+ await startWorkflow(getInternals(engine), type, input, options, additionalStartOperations, createLifecycleCallbacks(engine), void 0, revisionOverride);
25
25
  },
26
26
  loadWorkflowState: (workflowId) => loadWorkflowState(getInternals(engine), workflowId),
27
27
  cancelWorkflow: (workflowId) => engine.cancel(workflowId),
@@ -1,6 +1,7 @@
1
1
  import { restoreWorkflowCatalog, WorkflowCatalog } from "../catalog/index.js";
2
2
  import { buildWorkflowManifestForType } from "../registry-workflow-manifest.js";
3
3
  import { dispatchCatalogInstallAndActivatedEvents } from "./catalog-events.js";
4
+ import { resolveOrphanedCatalogTombstones } from "./catalog-tombstone-recovery.js";
4
5
  import { EngineDisposedError } from "./errors.js";
5
6
  import { getInternals } from "./internals.js";
6
7
  export { getWorkflowCatalog } from "./internals.js";
@@ -44,6 +45,7 @@ export async function ensureWorkflowCatalogReady(engine) {
44
45
  if (internals.disposed)
45
46
  throw new EngineDisposedError;
46
47
  if (!internals.catalogRestored) {
48
+ await resolveOrphanedCatalogTombstones(internals.storage);
47
49
  internals.workflowCatalog = new WorkflowCatalog(internals.storage, await restoreWorkflowCatalog(internals.storage));
48
50
  internals.catalogRestored = !0;
49
51
  }
@@ -15,6 +15,7 @@
15
15
  import { type WorkflowRevisionReferenceCounts } from '../catalog/index.ts';
16
16
  import type { Engine } from './index.ts';
17
17
  import { type EngineInternals } from './internals.ts';
18
+ type RegistrationEntry = EngineInternals['registrations'] extends Map<string, infer Entry> ? Entry : never;
18
19
  /**
19
20
  * Reserve one in-flight-start slot against `type`'s currently active
20
21
  * revision (if any), returning the revision reserved (or `undefined` when
@@ -25,15 +26,70 @@ import { type EngineInternals } from './internals.ts';
25
26
  * caller and passed back to {@link releaseInFlightStart} in its `finally` —
26
27
  * never re-resolved, so a concurrent activation moving the pointer
27
28
  * mid-start cannot decrement a different revision than was incremented.
29
+ *
30
+ * `revisionOverride` (WFT-15/16) is the revision `resolveExecutableRegistration()`
31
+ * already resolved for a `registerSource()`-registered `type` — passed
32
+ * through instead of re-deriving via `catalog.resolveActive(type)`, which
33
+ * would read `undefined` for a dynamic source with no catalog active
34
+ * pointer set (a source can resolve and install without ever being
35
+ * `engine.workflows.activate()`-d), making `inFlightStarts` diagnostics
36
+ * silently miss a lazy run in flight. `undefined` (the default) preserves
37
+ * the eager-path behavior byte-for-byte.
38
+ */
39
+ export declare function reserveInFlightStart(internals: EngineInternals, type: string, revisionOverride?: string): string | undefined;
40
+ /**
41
+ * `startWorkflow`'s combined "resolve `type`, reserve an `inFlightStarts`
42
+ * slot for the resolved revision" step. Reserves EARLY — before the loader
43
+ * is awaited, via `resolve()`'s `onRevisionChosen` hook — for a lazy type,
44
+ * closing the window where a concurrent `removeWorkflowRevision()` could
45
+ * observe zero references against a revision this call's own source load
46
+ * is about to durably (re)install; reserves afterward for an eager type,
47
+ * exactly as before WFT-15/16 (that path never invokes the hook). If the
48
+ * loader then fails — `resolve()` rejects AFTER the hook already fired — the
49
+ * early reservation is released here before rethrowing: `startWorkflow`
50
+ * only assigns its own `inFlightRevision` on a successful return, so its
51
+ * `finally` releases nothing on this path and the increment would otherwise
52
+ * leak forever, permanently reporting the revision non-`removable`.
53
+ *
54
+ * `resolvedRevision` is the raw value `resolve()` itself returned —
55
+ * `undefined` for an eager registration, the resolved candidate for a
56
+ * dynamic source — distinct from `inFlightRevision`, which for an eager
57
+ * type falls back to the catalog's cached ACTIVE pointer (see
58
+ * {@link reserveInFlightStart}). A caller that needs "the exact revision of
59
+ * the code this process is about to run" (WFT-17's `WorkflowState.revision`)
60
+ * must use `resolvedRevision`, not `inFlightRevision` — the two intentionally
61
+ * diverge for an eager type under a multi-engine deployment where this
62
+ * process's own registration lags the durable active pointer.
28
63
  */
29
- export declare function reserveInFlightStart(internals: EngineInternals, type: string): string | undefined;
64
+ export declare function resolveAndReserveExecutableRegistration(internals: EngineInternals, type: string, resolve: (type: string, onRevisionChosen?: (revision: string) => void) => Promise<{
65
+ entry: RegistrationEntry;
66
+ revision: string | undefined;
67
+ }>): Promise<{
68
+ registration: RegistrationEntry;
69
+ inFlightRevision: string | undefined;
70
+ resolvedRevision: string | undefined;
71
+ }>;
30
72
  /** Release the slot {@link reserveInFlightStart} reserved; a no-op when `revision` is `undefined`. */
31
73
  export declare function releaseInFlightStart(internals: EngineInternals, type: string, revision: string | undefined): void;
32
74
  /**
33
75
  * Count every in-process reference this batch wires to a real signal
34
76
  * against `(name, revision)`. `registeredDefinitions` and `inFlightStarts`
35
- * are real; the other five fields of {@link WorkflowRevisionReferenceCounts}
36
- * stay `0` until WFT-17 (see that type's own field-level docs).
77
+ * are in-memory, constant-time lookups; `nonTerminalRuns` (WFT-17 see
78
+ * {@link countNonTerminalRunsForRevision}) is an UNBOUNDED, full `wf:`-prefix
79
+ * durable scan — the same shape `groupActiveWorkflowsByType` already uses
80
+ * elsewhere for this class of diagnostic, so it's consistent with how this
81
+ * codebase already pays for this, but it is O(n) in the total number of
82
+ * workflow records, not bounded by `(name, revision)`. `removeWorkflowRevision()`
83
+ * calls this TWICE per removal attempt (its own pre-check, and
84
+ * {@link finalizeRevisionRemoval}'s post-check) — worth knowing before
85
+ * wiring `removeWorkflowRevision()` into an automated cleanup path on a
86
+ * large durable store, rather than the operator-triggered use this batch
87
+ * assumes. `pinnedSchedules` is ALSO an unbounded, full `schedule:`-prefix
88
+ * durable scan (WFT-20 — see {@link countPinnedSchedulesForRevision}), so
89
+ * this function now pays for two bounded-but-unbounded-in-store-size scans
90
+ * per call. The remaining two fields of {@link WorkflowRevisionReferenceCounts}
91
+ * stay `0` — the dispatch ledger and execution realms are out of this
92
+ * batch's scope.
37
93
  */
38
94
  export declare function countWorkflowRevisionReferences(engine: Engine, name: string, revision: string): Promise<WorkflowRevisionReferenceCounts>;
39
95
  /**
@@ -76,6 +132,39 @@ export type WorkflowCatalogRemovalResult = Readonly<{
76
132
  * compare-and-swap loses to a concurrent writer — the caller may re-read
77
133
  * and retry. Dispatches `catalog:revision-removed` on success.
78
134
  *
135
+ * Checks references BOTH before and after the durable delete (WFT-17,
136
+ * Codex review on PR #958). The pre-check above is an OPTIMIZATION — it
137
+ * refuses upfront in the common case, avoiding a needless delete-then-restore
138
+ * round trip — but is not, on its own, the guarantee: a concurrent
139
+ * `engine.start()` on a DIFFERENT process could read the entry as still
140
+ * installed, and commit a NEW run pinned to `revision`, in the narrow
141
+ * window between this function's pre-check scan and its own delete CAS
142
+ * landing; the pre-check's zero-references snapshot would then be stale
143
+ * by the time the delete actually commits. The POST-check closes this: it
144
+ * is safe to trust because `start-commit.ts`'s own start batch fences on
145
+ * this exact catalog entry's bytes (`buildCatalogEntryStartPrecondition`)
146
+ * — `conditionalBatch`'s precondition is evaluated against LIVE storage
147
+ * state at commit time, so ANY start whose commit lands after this
148
+ * function's delete necessarily loses its own CAS and never creates the
149
+ * reference at all. A nonzero post-delete count can therefore only be a
150
+ * run that committed BEFORE the delete — this function restores the entry
151
+ * and reports `'referenced'` rather than leaving a real, still-referenced
152
+ * run pinned to a revision the catalog no longer carries.
153
+ *
154
+ * `catalog.remove()`'s delete and `finalizeRevisionRemoval`'s restore-or-
155
+ * finalize used to be two SEPARATE commits (WFT-17/18, Codex review on PR
156
+ * #958): a process crash between them left the revision durably uninstalled
157
+ * with NO durable record of what was deleted, recoverable by no one. Both
158
+ * halves of `removeCatalogEntry` (the delete) now land in ONE
159
+ * `conditionalBatch` with a `catalog-tombstone:<name>:<revision>` record
160
+ * (the exact deleted bytes) — see `core/catalog/removal.ts`'s module doc —
161
+ * so this function's own restore-or-finalize below is itself a SECOND
162
+ * atomic operation (not a bare `install()`/no-op), and a crash between the
163
+ * two commits leaves a durable tombstone ANY process can resolve, not just
164
+ * this one. `catalog-tombstone-recovery.ts`'s boot-time sweep and this
165
+ * function's own targeted pre-check (below) both close that residual
166
+ * window; see that module's doc for the full mechanism.
167
+ *
79
168
  * @example
80
169
  * ```ts
81
170
  * import { Engine, removeWorkflowRevision, workflow } from '@lostgradient/weft';
@@ -111,6 +200,20 @@ export type WorkflowRevisionDiagnostics = Readonly<{
111
200
  activeRevision?: string;
112
201
  references: WorkflowRevisionReferenceCounts;
113
202
  removable: boolean;
203
+ /**
204
+ * Dynamic-source load diagnostics (WFT-15/16), present only when `name`
205
+ * has an entry in `internals.sources.byName` — i.e. `name` has ever been
206
+ * `registerSource()`-registered on this engine. `undefined` for a purely
207
+ * eager name, or a dynamic source this process has never heard of.
208
+ */
209
+ source?: {
210
+ kind: import('../source/index.ts').WorkflowSourceKind;
211
+ requestedRevision: string;
212
+ state: import('./source-runtime-state.ts').SourceLoadState;
213
+ loadDurationMs?: number;
214
+ lastFailureCategory?: import('../types/identity.ts').FailureCategory;
215
+ waiterCount: number;
216
+ };
114
217
  }>;
115
218
  /**
116
219
  * Bounded diagnostics for one `(name, revision)`: whether it is installed,
@@ -130,3 +233,4 @@ export type WorkflowRevisionDiagnostics = Readonly<{
130
233
  * ```
131
234
  */
132
235
  export declare function getWorkflowRevisionDiagnostics(engine: Engine, name: string, revision: string): Promise<WorkflowRevisionDiagnostics>;
236
+ export {};
@@ -1,29 +1,47 @@
1
1
  import {
2
2
  decrementNestedRevisionCount,
3
+ finalizeCatalogTombstone,
3
4
  incrementNestedRevisionCount,
4
5
  readNestedRevisionCount,
6
+ restoreCatalogEntryFromTombstone,
5
7
  totalWorkflowRevisionReferences
6
8
  } from "../catalog/index.js";
7
9
  import { WorkflowRevisionRemovedEvent } from "../events/catalog-events.js";
8
10
  import { ensureWorkflowCatalogReady, getWorkflowCatalog } from "./catalog-readiness.js";
11
+ import { resolveCatalogTombstoneIfPresent } from "./catalog-tombstone-recovery.js";
9
12
  import { getInternals } from "./internals.js";
10
- export function reserveInFlightStart(internals, type) {
11
- const revision = internals.workflowCatalog?.resolveActive(type)?.revision;
13
+ import { countNonTerminalRunsForRevision } from "./nonterminal-revision-count.js";
14
+ import { countPinnedSchedulesForRevision } from "./pinned-schedule-revision-count.js";
15
+ import { readSourceLoadDiagnostics, readSourceWaiterCount } from "./source-diagnostics.js";
16
+ export function reserveInFlightStart(internals, type, revisionOverride) {
17
+ const revision = revisionOverride ?? internals.workflowCatalog?.resolveActive(type)?.revision;
12
18
  if (revision !== void 0)
13
19
  incrementNestedRevisionCount(internals.inFlightStartsByRevision, type, revision);
14
20
  return revision;
15
21
  }
22
+ export async function resolveAndReserveExecutableRegistration(internals, type, resolve) {
23
+ let earlyReservation;
24
+ try {
25
+ const { entry: registration, revision } = await resolve(type, (chosen) => {
26
+ earlyReservation = reserveInFlightStart(internals, type, chosen);
27
+ }), inFlightRevision = earlyReservation !== void 0 ? earlyReservation : reserveInFlightStart(internals, type, revision);
28
+ return { registration, inFlightRevision, resolvedRevision: revision };
29
+ } catch (error) {
30
+ releaseInFlightStart(internals, type, earlyReservation);
31
+ throw error;
32
+ }
33
+ }
16
34
  export function releaseInFlightStart(internals, type, revision) {
17
35
  if (revision !== void 0)
18
36
  decrementNestedRevisionCount(internals.inFlightStartsByRevision, type, revision);
19
37
  }
20
38
  export async function countWorkflowRevisionReferences(engine, name, revision) {
21
- const internals = getInternals(engine);
39
+ const internals = getInternals(engine), nonTerminalRuns = await countNonTerminalRunsForRevision(internals.storage, name, revision), pinnedSchedules = await countPinnedSchedulesForRevision(internals.storage, name, revision);
22
40
  return {
23
41
  registeredDefinitions: internals.registeredCatalogRevisions.get(name) === revision ? 1 : 0,
24
42
  inFlightStarts: readNestedRevisionCount(internals.inFlightStartsByRevision, name, revision),
25
- nonTerminalRuns: 0,
26
- pinnedSchedules: 0,
43
+ nonTerminalRuns,
44
+ pinnedSchedules,
27
45
  pendingDispatches: 0,
28
46
  activeExecutionRealms: 0,
29
47
  retainedRecoveryRecords: 0
@@ -31,20 +49,20 @@ export async function countWorkflowRevisionReferences(engine, name, revision) {
31
49
  }
32
50
  export async function removeWorkflowRevision(engine, name, revision) {
33
51
  await ensureWorkflowCatalogReady(engine);
34
- const catalog = getWorkflowCatalog(engine);
52
+ const catalog = getWorkflowCatalog(engine), internals = getInternals(engine);
53
+ await resolveCatalogTombstoneIfPresent(internals.storage, name, revision);
35
54
  if (!await catalog.hasInstalled(name, revision))
36
55
  return { removed: !1, reason: "not-found" };
37
56
  const active = await catalog.resolveActiveDurable(name);
38
57
  if (active !== void 0 && active.revision === revision)
39
58
  return { removed: !1, reason: "active", activeRevision: active.revision };
40
- const references = await countWorkflowRevisionReferences(engine, name, revision);
41
- if (totalWorkflowRevisionReferences(references) > 0)
42
- return { removed: !1, reason: "referenced", references };
59
+ const preReferences = await countWorkflowRevisionReferences(engine, name, revision);
60
+ if (totalWorkflowRevisionReferences(preReferences) > 0)
61
+ return { removed: !1, reason: "referenced", references: preReferences };
43
62
  const result = await catalog.remove(name, revision);
44
63
  switch (result.outcome) {
45
64
  case "removed":
46
- engine.dispatchEvent(new WorkflowRevisionRemovedEvent(name, revision));
47
- return { removed: !0 };
65
+ return finalizeRevisionRemoval(engine, name, revision, result.tombstoneBytes);
48
66
  case "not-found":
49
67
  return { removed: !1, reason: "not-found" };
50
68
  case "active":
@@ -55,9 +73,19 @@ export async function removeWorkflowRevision(engine, name, revision) {
55
73
  throw Error(`Unknown workflow catalog removal outcome: ${String(result)}`);
56
74
  }
57
75
  }
76
+ async function finalizeRevisionRemoval(engine, name, revision, tombstoneBytes) {
77
+ const internals = getInternals(engine), postReferences = await countWorkflowRevisionReferences(engine, name, revision);
78
+ if (totalWorkflowRevisionReferences(postReferences) > 0) {
79
+ await restoreCatalogEntryFromTombstone(internals.storage, name, revision, tombstoneBytes);
80
+ return { removed: !1, reason: "referenced", references: postReferences };
81
+ }
82
+ await finalizeCatalogTombstone(internals.storage, name, revision, tombstoneBytes);
83
+ engine.dispatchEvent(new WorkflowRevisionRemovedEvent(name, revision));
84
+ return { removed: !0 };
85
+ }
58
86
  export async function getWorkflowRevisionDiagnostics(engine, name, revision) {
59
87
  await ensureWorkflowCatalogReady(engine);
60
- const catalog = getWorkflowCatalog(engine), installed = await catalog.hasInstalled(name, revision), activePointer = await catalog.resolveActiveDurable(name), active = activePointer !== void 0 && activePointer.revision === revision, references = await countWorkflowRevisionReferences(engine, name, revision), removable = installed && !active && totalWorkflowRevisionReferences(references) === 0;
88
+ const catalog = getWorkflowCatalog(engine), installed = await catalog.hasInstalled(name, revision), activePointer = await catalog.resolveActiveDurable(name), active = activePointer !== void 0 && activePointer.revision === revision, references = await countWorkflowRevisionReferences(engine, name, revision), removable = installed && !active && totalWorkflowRevisionReferences(references) === 0, source = buildSourceDiagnostics(getInternals(engine), name, revision);
61
89
  return {
62
90
  name,
63
91
  revision,
@@ -65,6 +93,31 @@ export async function getWorkflowRevisionDiagnostics(engine, name, revision) {
65
93
  active,
66
94
  ...activePointer !== void 0 ? { activeRevision: activePointer.revision } : {},
67
95
  references,
68
- removable
96
+ removable,
97
+ ...source !== void 0 ? { source } : {}
98
+ };
99
+ }
100
+ function resolveDiagnosticsSourceKind(diagnostics, registeredRevisions, revision) {
101
+ if (diagnostics !== void 0)
102
+ return diagnostics.kind;
103
+ const registered = registeredRevisions.get(revision);
104
+ return registered === void 0 ? "module" : registered.descriptor.kind;
105
+ }
106
+ function buildSourceDiagnostics(internals, name, revision) {
107
+ const registeredRevisions = internals.sources.byName.get(name);
108
+ if (registeredRevisions === void 0)
109
+ return;
110
+ const diagnostics = readSourceLoadDiagnostics(internals, name, revision);
111
+ return {
112
+ kind: resolveDiagnosticsSourceKind(diagnostics, registeredRevisions, revision),
113
+ requestedRevision: revision,
114
+ state: diagnostics === void 0 ? "idle" : diagnostics.state,
115
+ ...diagnostics?.loadDurationMs !== void 0 && {
116
+ loadDurationMs: diagnostics.loadDurationMs
117
+ },
118
+ ...diagnostics?.lastFailureCategory !== void 0 && {
119
+ lastFailureCategory: diagnostics.lastFailureCategory
120
+ },
121
+ waiterCount: readSourceWaiterCount(internals, name, revision)
69
122
  };
70
123
  }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Resolves durable `catalog-tombstone:<name>:<revision>` records left
3
+ * behind by {@link import('../catalog/removal.ts').removeCatalogEntry}
4
+ * (WFT-17/18, Codex review on PR #958) — a tombstone exists only in the
5
+ * brief window between its delete-and-tombstone commit and the SAME call's
6
+ * own resolution (restore or finalize), normally sub-millisecond. A
7
+ * tombstone that outlives that window is an ORPHAN: the process that
8
+ * created it crashed (or otherwise failed) before resolving it. This
9
+ * module makes that recoverable by ANY process, not just the one that
10
+ * crashed — the exact gap the tombstone design closes (see
11
+ * `core/catalog/removal.ts`'s module doc for the full mechanism).
12
+ *
13
+ * Deliberately lives in `core/engine/`, not alongside the tombstone
14
+ * primitives in `core/catalog/`: resolving an orphan needs a FRESH
15
+ * reference count, and the only reference signal meaningful for a
16
+ * crashed peer's tombstone is the durable one —
17
+ * `nonterminal-revision-count.ts`'s `wf:`-prefix scan (in-process signals
18
+ * like `registeredDefinitions`/`inFlightStarts` are per-engine-instance and
19
+ * say nothing about what a DIFFERENT, crashed process was doing).
20
+ * `core/catalog/**` never imports from `core/engine/**` — a directional
21
+ * boundary `check-import-cycles.ts` enforces — so this orchestration has to
22
+ * live on the engine side of that line.
23
+ *
24
+ * Two call sites: `catalog-readiness.ts`'s boot-time sweep (every
25
+ * orphaned tombstone in the whole store, run once per engine instance
26
+ * before recovery or any new start can observe stale state) and
27
+ * `catalog-removal.ts`'s `removeWorkflowRevision` (a single, targeted
28
+ * check for the EXACT `(name, revision)` key it is about to act on, since
29
+ * a long-lived engine could observe a peer crash mid-lifetime, after its
30
+ * own boot sweep already ran).
31
+ *
32
+ * @module core/engine/catalog-tombstone-recovery
33
+ */
34
+ import { type Storage } from '../../storage/interface.ts';
35
+ /**
36
+ * Sweep every `catalog-tombstone:` record in `storage` and resolve each
37
+ * one. Bounded by the number of orphaned tombstones actually present —
38
+ * normally zero, so this is a cheap prefix scan that yields nothing to
39
+ * iterate; only non-trivial after a real crash left orphans behind. Called
40
+ * once per engine instance, at catalog-boot time
41
+ * (`catalog-readiness.ts`), before recovery's own preflight or any fresh
42
+ * start can observe a revision this sweep would otherwise still be
43
+ * resolving.
44
+ */
45
+ export declare function resolveOrphanedCatalogTombstones(storage: Storage): Promise<void>;
46
+ /**
47
+ * Targeted check for ONE `(name, revision)` tombstone — used by
48
+ * `removeWorkflowRevision` immediately before it acts, so a peer crash
49
+ * that happened after this engine's own boot-time sweep already ran (a
50
+ * long-lived engine) does not leave a stale tombstone permanently blocking
51
+ * every future removal attempt on that exact key (`removeCatalogEntry`'s
52
+ * own CAS requires the tombstone key absent). A no-op when no tombstone is
53
+ * present for this exact key — the overwhelmingly common case, costing one
54
+ * `storage.get`.
55
+ */
56
+ export declare function resolveCatalogTombstoneIfPresent(storage: Storage, name: string, revision: string): Promise<void>;