@lostgradient/weft 0.12.0 → 0.14.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 (226) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/api.d.ts +2 -0
  3. package/dist/cli/api.js +3 -4
  4. package/dist/cli/generated/operation-catalog.snapshot.json +508 -12
  5. package/dist/cli/generated/operation-client.generated.d.ts +118 -10
  6. package/dist/cli/generated/operation-client.generated.js +74 -1
  7. package/dist/cli/help-text.d.ts +3 -3
  8. package/dist/cli/help-text.js +4 -4
  9. package/dist/cli/operation-catalog-snapshot.d.ts +12 -3
  10. package/dist/cli/operation-catalog-snapshot.js +14 -5
  11. package/dist/cli/operation-client-runtime.d.ts +29 -0
  12. package/dist/cli/schedule.js +1 -3
  13. package/dist/cli/serve-registrations.d.ts +3 -3
  14. package/dist/cli/serve-registrations.js +2 -2
  15. package/dist/cli/shutdown.d.ts +11 -0
  16. package/dist/cli/shutdown.js +25 -0
  17. package/dist/cli/validate.d.ts +1 -1
  18. package/dist/cli/version-check.d.ts +1 -1
  19. package/dist/cli/version-check.js +1 -1
  20. package/dist/cli-main.js +10 -29
  21. package/dist/client/client-storage.d.ts +24 -0
  22. package/dist/client/client-storage.js +14 -0
  23. package/dist/client/handle-delegation.d.ts +3 -3
  24. package/dist/client/handle-delegation.js +2 -2
  25. package/dist/client/http-client-requests.d.ts +5 -3
  26. package/dist/client/http-client-requests.js +20 -2
  27. package/dist/client/http-client-storage.d.ts +3 -0
  28. package/dist/client/http-client-storage.js +82 -0
  29. package/dist/client/http-client.d.ts +9 -6
  30. package/dist/client/http-client.js +13 -18
  31. package/dist/client/http-operations.d.ts +3 -1
  32. package/dist/client/http-operations.js +77 -1
  33. package/dist/client/http-request.d.ts +20 -14
  34. package/dist/client/http-request.js +21 -31
  35. package/dist/client/in-process-operations.d.ts +2 -2
  36. package/dist/client/in-process-operations.js +2 -2
  37. package/dist/client/index.d.ts +5 -1
  38. package/dist/client/index.js +13 -2
  39. package/dist/client/interface.d.ts +26 -26
  40. package/dist/client/local.d.ts +9 -6
  41. package/dist/client/local.js +9 -4
  42. package/dist/client/search-params.js +10 -6
  43. package/dist/client/start-body.d.ts +3 -1
  44. package/dist/client/start-body.js +9 -0
  45. package/dist/core/bulk-workflow-filter.d.ts +2 -1
  46. package/dist/core/bulk-workflow-filter.js +5 -1
  47. package/dist/core/context/parallel-operations.d.ts +0 -2
  48. package/dist/core/context/parallel-operations.js +0 -7
  49. package/dist/core/engine/async-activity-records.d.ts +12 -1
  50. package/dist/core/engine/async-activity-records.js +81 -2
  51. package/dist/core/engine/bulk-operations-purge.js +16 -1
  52. package/dist/core/engine/bulk-operations-shared.js +2 -0
  53. package/dist/core/engine/callback-checkpoint-persistence.d.ts +2 -0
  54. package/dist/core/engine/callback-checkpoint-persistence.js +6 -3
  55. package/dist/core/engine/callback-creators-schedule.d.ts +2 -1
  56. package/dist/core/engine/callback-creators-schedule.js +3 -3
  57. package/dist/core/engine/callback-creators.js +4 -22
  58. package/dist/core/engine/checkpoint-io.d.ts +1 -1
  59. package/dist/core/engine/checkpoint-reads.js +11 -1
  60. package/dist/core/engine/child-workflow.js +15 -6
  61. package/dist/core/engine/construction.js +3 -0
  62. package/dist/core/engine/disposal.js +2 -0
  63. package/dist/core/engine/engine-internal-types.d.ts +4 -1
  64. package/dist/core/engine/fenced-write.d.ts +5 -3
  65. package/dist/core/engine/finalizer-status.d.ts +10 -0
  66. package/dist/core/engine/finalizer-status.js +82 -0
  67. package/dist/core/engine/index.d.ts +26 -2
  68. package/dist/core/engine/index.js +71 -8
  69. package/dist/core/engine/internals.d.ts +3 -0
  70. package/dist/core/engine/lease-health.d.ts +64 -0
  71. package/dist/core/engine/lease-health.js +0 -0
  72. package/dist/core/engine/lease-manager.d.ts +4 -2
  73. package/dist/core/engine/lease-manager.js +32 -4
  74. package/dist/core/engine/lifecycle/recovered-services.js +0 -2
  75. package/dist/core/engine/lifecycle/shared.d.ts +11 -8
  76. package/dist/core/engine/lifecycle/start-batch.js +12 -0
  77. package/dist/core/engine/lifecycle/start.d.ts +1 -1
  78. package/dist/core/engine/lifecycle/start.js +34 -5
  79. package/dist/core/engine/operations-coordination.d.ts +1 -12
  80. package/dist/core/engine/operations-coordination.js +58 -11
  81. package/dist/core/engine/operations-speculate.d.ts +1 -1
  82. package/dist/core/engine/operations-speculate.js +19 -5
  83. package/dist/core/engine/parallel-dispatch.d.ts +1 -1
  84. package/dist/core/engine/parallel-dispatch.js +1 -1
  85. package/dist/core/engine/registration.js +14 -0
  86. package/dist/core/engine/review-list-entries.d.ts +1 -1
  87. package/dist/core/engine/review-list-entries.js +6 -30
  88. package/dist/core/engine/schedule-handle.d.ts +4 -4
  89. package/dist/core/engine/schedule-handle.js +2 -2
  90. package/dist/core/engine/schedule-overlap.d.ts +11 -0
  91. package/dist/core/engine/schedule-overlap.js +50 -0
  92. package/dist/core/engine/schedule-run.d.ts +11 -3
  93. package/dist/core/engine/schedule-run.js +27 -4
  94. package/dist/core/engine/schedule-timer.js +10 -1
  95. package/dist/core/engine/schedules.d.ts +5 -4
  96. package/dist/core/engine/schedules.js +62 -56
  97. package/dist/core/engine/state-utilities.js +1 -1
  98. package/dist/core/engine/storage-io.d.ts +12 -0
  99. package/dist/core/engine/storage-io.js +49 -10
  100. package/dist/core/engine/termination/finalizer-claim.d.ts +8 -6
  101. package/dist/core/engine/termination/finalizer-claim.js +2 -1
  102. package/dist/core/engine/termination/finalizer.js +10 -14
  103. package/dist/core/engine/timeline-coordinator-constants.d.ts +3 -0
  104. package/dist/core/engine/timeline-coordinator-constants.js +1 -0
  105. package/dist/core/engine/timeline-coordinator-detail.d.ts +13 -0
  106. package/dist/core/engine/timeline-coordinator-detail.js +76 -0
  107. package/dist/core/engine/validation/schedule-options.d.ts +8 -1
  108. package/dist/core/engine/validation/schedule-options.js +18 -0
  109. package/dist/core/engine/validation/schedule.d.ts +1 -1
  110. package/dist/core/engine/validation/schedule.js +27 -3
  111. package/dist/core/engine/validation.js +66 -2
  112. package/dist/core/engine/workflow-state-stream.d.ts +5 -1
  113. package/dist/core/engine/workflow-state-stream.js +37 -14
  114. package/dist/core/events/schedule-events.d.ts +3 -4
  115. package/dist/core/events/workflow-events.d.ts +2 -1
  116. package/dist/core/list-filter-validation.d.ts +3 -0
  117. package/dist/core/list-filter-validation.js +11 -0
  118. package/dist/core/registry-snapshot.d.ts +32 -0
  119. package/dist/core/registry-snapshot.js +28 -0
  120. package/dist/core/review/index.d.ts +91 -4
  121. package/dist/core/review/index.js +29 -0
  122. package/dist/core/types/activity.d.ts +52 -5
  123. package/dist/core/types/bulk.d.ts +1 -0
  124. package/dist/core/types/list-options.d.ts +6 -0
  125. package/dist/core/types/options.d.ts +6 -5
  126. package/dist/core/types/reviews.d.ts +2 -2
  127. package/dist/core/types/schedules.d.ts +45 -2
  128. package/dist/core/types/services-resolution.d.ts +5 -5
  129. package/dist/core/types/state.d.ts +64 -0
  130. package/dist/core/types/workflow-log.d.ts +3 -2
  131. package/dist/core/types/workflow-observability.d.ts +50 -0
  132. package/dist/core/types/workflow-observability.js +0 -0
  133. package/dist/core/types/workflow-registry.d.ts +7 -0
  134. package/dist/core/types.d.ts +1 -0
  135. package/dist/core/types.js +1 -0
  136. package/dist/diagnostics/validate.d.ts +9 -30
  137. package/dist/diagnostics/validate.js +83 -21
  138. package/dist/diagnostics/version-check.d.ts +2 -2
  139. package/dist/http.js +2 -2
  140. package/dist/index.d.ts +4 -3
  141. package/dist/indexeddb.js +1 -1
  142. package/dist/server/access-policy-metadata.d.ts +92 -0
  143. package/dist/server/access-policy-metadata.js +78 -0
  144. package/dist/server/fault-to-http.d.ts +3 -7
  145. package/dist/server/fault-to-http.js +2 -34
  146. package/dist/server/handler/index.js +1 -1
  147. package/dist/server/handler/route-dispatch.d.ts +10 -3
  148. package/dist/server/handler/route-dispatch.js +19 -4
  149. package/dist/server/index.d.ts +1 -1
  150. package/dist/server/index.js +1 -0
  151. package/dist/server/interactive-operations.js +2 -0
  152. package/dist/server/openapi-error-responses.js +41 -4
  153. package/dist/server/openapi.d.ts +3 -1
  154. package/dist/server/openapi.js +4 -0
  155. package/dist/server/openrpc-document-schema.d.ts +64 -0
  156. package/dist/server/openrpc-document-schema.js +6 -26
  157. package/dist/server/openrpc.d.ts +4 -0
  158. package/dist/server/openrpc.js +9 -33
  159. package/dist/server/operation-catalog/index.d.ts +1 -1
  160. package/dist/server/operation-catalog/raise-fault.d.ts +2 -1
  161. package/dist/server/operation-catalog/types.d.ts +2 -1
  162. package/dist/server/operation-fault.d.ts +22 -8
  163. package/dist/server/operation-fault.js +65 -13
  164. package/dist/server/operation-registry.d.ts +1 -1
  165. package/dist/server/operations/aggregate-workflows.d.ts +6 -0
  166. package/dist/server/operations/aggregate-workflows.js +3 -2
  167. package/dist/server/operations/async-activity.d.ts +14 -0
  168. package/dist/server/operations/async-activity.js +88 -4
  169. package/dist/server/operations/bulk-filter-helpers.d.ts +3 -0
  170. package/dist/server/operations/bulk-filter-helpers.js +27 -3
  171. package/dist/server/operations/bulk-mutate-workflow-tags.d.ts +6 -0
  172. package/dist/server/operations/bulk-signal-workflows.d.ts +6 -0
  173. package/dist/server/operations/get-stream-chunks.js +2 -7
  174. package/dist/server/operations/get-system-lease.d.ts +19 -0
  175. package/dist/server/operations/get-system-lease.js +48 -0
  176. package/dist/server/operations/get-workflow-observability.d.ts +24 -0
  177. package/dist/server/operations/get-workflow-observability.js +80 -0
  178. package/dist/server/operations/get-workflow-result.js +2 -2
  179. package/dist/server/operations/get-workflow.d.ts +2 -2
  180. package/dist/server/operations/list-filter-query-extractor.js +15 -0
  181. package/dist/server/operations/list-reviews.js +2 -19
  182. package/dist/server/operations/list-workflows.d.ts +6 -0
  183. package/dist/server/operations/list-workflows.js +12 -3
  184. package/dist/server/operations/operation-helpers.d.ts +9 -18
  185. package/dist/server/operations/operation-helpers.js +5 -23
  186. package/dist/server/operations/purge-workflows.d.ts +3 -0
  187. package/dist/server/operations/recover-all.js +1 -9
  188. package/dist/server/operations/single-workflow-control-operation.d.ts +2 -1
  189. package/dist/server/operations/single-workflow-tag-mutation.js +2 -2
  190. package/dist/server/operations/sse-stream.js +2 -7
  191. package/dist/server/operations/storage-capabilities.d.ts +34 -0
  192. package/dist/server/operations/storage-capabilities.js +38 -0
  193. package/dist/server/operations/storage.js +8 -17
  194. package/dist/server/operations/stream-workflow-sse.js +3 -9
  195. package/dist/server/operations/update-schedule.d.ts +8 -0
  196. package/dist/server/operations/update-schedule.js +70 -7
  197. package/dist/server/operations/workflow-events-sse.js +2 -2
  198. package/dist/server/rest-binding.d.ts +7 -6
  199. package/dist/server/rest-bindings.d.ts +2 -13
  200. package/dist/server/rest-bindings.js +19 -10
  201. package/dist/server/task-queue.d.ts +7 -5
  202. package/dist/storage/bounded-ndjson-response.d.ts +5 -0
  203. package/dist/storage/bounded-ndjson-response.js +33 -0
  204. package/dist/storage/capabilities.d.ts +1 -1
  205. package/dist/storage/cloudflare-value-codec.d.ts +5 -3
  206. package/dist/storage/cloudflare-value-codec.js +1 -1
  207. package/dist/storage/compressed-storage.js +1 -1
  208. package/dist/storage/http.js +5 -37
  209. package/dist/storage/index.d.ts +7 -1
  210. package/dist/storage/interface.d.ts +16 -4
  211. package/dist/storage/interface.js +1 -1
  212. package/dist/storage/key-prefixes.d.ts +1 -1
  213. package/dist/storage/key-prefixes.js +4 -0
  214. package/dist/storage/lmdb.js +1 -1
  215. package/dist/storage/memory.js +1 -1
  216. package/dist/storage/neon.d.ts +3 -16
  217. package/dist/storage/neon.js +4 -4
  218. package/dist/storage/postgres.js +4 -4
  219. package/dist/storage/resolve.js +1 -1
  220. package/dist/storage/scoped-storage.js +1 -1
  221. package/dist/storage/testing.js +1 -1
  222. package/dist/storage/turso.js +2 -2
  223. package/dist/version.d.ts +1 -1
  224. package/dist/version.js +1 -1
  225. package/dist/web-extension.js +1 -1
  226. package/package.json +1 -1
@@ -1,32 +1,8 @@
1
- import { z } from "zod";
2
1
  import { decode } from "../codec.js";
3
- const storedReviewRequestSchema = z.object({
4
- reviewId: z.string(),
5
- workflowId: z.string(),
6
- artifact: z.unknown().nonoptional(),
7
- reviewType: z.string(),
8
- reviewers: z.array(z.string()),
9
- allowPartial: z.boolean(),
10
- timeout: z.number().optional(),
11
- webhookUrl: z.string().optional(),
12
- createdAt: z.number()
13
- }), persistedCompletedReviewEntrySchema = z.object({
14
- status: z.literal("completed"),
15
- reviewId: z.string(),
16
- workflowId: z.string(),
17
- artifact: z.unknown().nonoptional(),
18
- reviewType: z.string(),
19
- reviewers: z.array(z.string()),
20
- allowPartial: z.boolean(),
21
- timeout: z.number().optional(),
22
- webhookUrl: z.string().optional(),
23
- createdAt: z.number(),
24
- decision: z.enum(["approved", "rejected", "needs-changes"]),
25
- reviewer: z.string(),
26
- feedback: z.string().optional(),
27
- sectionDecisions: z.record(z.string(), z.enum(["approved", "rejected"])).optional(),
28
- timestamp: z.number()
29
- });
2
+ import {
3
+ completedReviewEntrySchema,
4
+ reviewRequestSchema
5
+ } from "../review/index.js";
30
6
  function assignWhenDefined(target, key, value) {
31
7
  if (value !== void 0)
32
8
  target[key] = value;
@@ -58,7 +34,7 @@ export function parseStoredReviewRequest(value) {
58
34
  } catch {
59
35
  return null;
60
36
  }
61
- const parsedReview = storedReviewRequestSchema.safeParse(decodedValue);
37
+ const parsedReview = reviewRequestSchema.safeParse(decodedValue);
62
38
  if (!parsedReview.success)
63
39
  return null;
64
40
  return normalizeStoredReviewRequest(parsedReview.data);
@@ -110,7 +86,7 @@ export function parseCompletedReviewEntry(value) {
110
86
  } catch {
111
87
  return null;
112
88
  }
113
- const parsedReview = persistedCompletedReviewEntrySchema.safeParse(decodedValue);
89
+ const parsedReview = completedReviewEntrySchema.safeParse(decodedValue);
114
90
  if (!parsedReview.success)
115
91
  return null;
116
92
  return normalizeCompletedReviewEntry(parsedReview.data);
@@ -1,4 +1,4 @@
1
- import type { ScheduleSpec, ScheduleSummary } from '../types.ts';
1
+ import type { ScheduleSpec, ScheduleSummary, ScheduleUpdateOptions } from '../types.ts';
2
2
  /**
3
3
  * Narrow engine view a {@link ScheduleHandle} delegates to. The full
4
4
  * {@link Engine} implements it; the handle only depends on these schedule
@@ -8,13 +8,13 @@ export interface ScheduleHandleEngine {
8
8
  pauseSchedule(scheduleId: string): Promise<void>;
9
9
  resumeSchedule(scheduleId: string): Promise<void>;
10
10
  cancelSchedule(scheduleId: string): Promise<void>;
11
- updateSchedule(scheduleId: string, newSpec: string | ScheduleSpec): Promise<void>;
11
+ updateSchedule(scheduleId: string, newSpec: string | ScheduleSpec, options?: ScheduleUpdateOptions): Promise<void>;
12
12
  getSchedule(scheduleId: string): Promise<ScheduleSummary | null>;
13
13
  }
14
14
  /**
15
15
  * Handle to a recurring schedule created by {@link Engine.schedule}. Use
16
16
  * `handle.pause()`, `handle.resume()`, `handle.cancel()`, or
17
- * `handle.update(cronExpression)` to manage the schedule lifecycle.
17
+ * `handle.update(spec, options?)` to manage the schedule lifecycle.
18
18
  * `handle.describe()` returns the current {@link ScheduleSummary}.
19
19
  *
20
20
  * @example
@@ -40,6 +40,6 @@ export declare class ScheduleHandle {
40
40
  pause(): Promise<void>;
41
41
  resume(): Promise<void>;
42
42
  cancel(): Promise<void>;
43
- update(newSpec: string | ScheduleSpec): Promise<void>;
43
+ update(newSpec: string | ScheduleSpec, options?: ScheduleUpdateOptions): Promise<void>;
44
44
  describe(): Promise<ScheduleSummary>;
45
45
  }
@@ -14,8 +14,8 @@ export class ScheduleHandle {
14
14
  async cancel() {
15
15
  await this.#engine.cancelSchedule(this.id);
16
16
  }
17
- async update(newSpec) {
18
- await this.#engine.updateSchedule(this.id, newSpec);
17
+ async update(newSpec, options) {
18
+ await this.#engine.updateSchedule(this.id, newSpec, options);
19
19
  }
20
20
  async describe() {
21
21
  const schedule = await this.#engine.getSchedule(this.id);
@@ -0,0 +1,11 @@
1
+ import type { ScheduleState } from '../types.ts';
2
+ import type { EngineInternals } from './internals.ts';
3
+ import type { ScheduledRunStartOptions } from './schedule-run.ts';
4
+ type ScheduleOverlapCallbacks = {
5
+ cancelWorkflow: (workflowId: string) => Promise<void>;
6
+ getWorkflowResult: (workflowId: string) => Promise<unknown>;
7
+ startScheduledRun: (state: ScheduleState, options?: ScheduledRunStartOptions) => Promise<string>;
8
+ };
9
+ export declare function applyBlockedScheduleOccurrence(internals: EngineInternals, state: ScheduleState, hasActiveWorkflow: boolean, callbacks: ScheduleOverlapCallbacks, occurrence?: number): Promise<ScheduleState>;
10
+ export declare function drainQueuedScheduleRun(state: ScheduleState, callbacks: Pick<ScheduleOverlapCallbacks, 'startScheduledRun'>, completedWorkflowId?: string): Promise<ScheduleState>;
11
+ export {};
@@ -0,0 +1,50 @@
1
+ import { clearScheduleCurrentWorkflow } from "./state-utilities.js";
2
+ export async function applyBlockedScheduleOccurrence(internals, state, hasActiveWorkflow, callbacks, occurrence) {
3
+ if (!hasActiveWorkflow)
4
+ return startScheduleRunInOccupiedSlot(state, callbacks, occurrence);
5
+ if (state.overlap === "cancel-running") {
6
+ if (state.currentWorkflowId) {
7
+ callbacks.getWorkflowResult(state.currentWorkflowId).catch(() => {});
8
+ await callbacks.cancelWorkflow(state.currentWorkflowId);
9
+ }
10
+ return startScheduleRunInOccupiedSlot(clearScheduleCurrentWorkflow(state), callbacks, occurrence);
11
+ }
12
+ if (state.overlap === "queue")
13
+ return {
14
+ ...state,
15
+ queuedRuns: [
16
+ ...state.queuedRuns,
17
+ {
18
+ workflowId: crypto.randomUUID(),
19
+ queuedAt: internals.options.getNow(),
20
+ ...occurrence !== void 0 && { occurrence }
21
+ }
22
+ ]
23
+ };
24
+ return state;
25
+ }
26
+ async function startScheduleRunInOccupiedSlot(state, callbacks, occurrence) {
27
+ const workflowId = crypto.randomUUID(), stateAfterStart = { ...state, currentWorkflowId: workflowId };
28
+ await callbacks.startScheduledRun(state, {
29
+ workflowId,
30
+ ...occurrence !== void 0 && { occurrence }
31
+ });
32
+ return stateAfterStart;
33
+ }
34
+ export async function drainQueuedScheduleRun(state, callbacks, completedWorkflowId) {
35
+ const [queuedRun, ...remainingQueuedRuns] = state.queuedRuns;
36
+ if (queuedRun === void 0)
37
+ return state;
38
+ const stateAfterStart = {
39
+ ...state,
40
+ currentWorkflowId: queuedRun.workflowId,
41
+ queuedRuns: remainingQueuedRuns
42
+ };
43
+ await callbacks.startScheduledRun(state, {
44
+ workflowId: queuedRun.workflowId,
45
+ ...queuedRun.occurrence !== void 0 && { occurrence: queuedRun.occurrence },
46
+ scheduleStateAfterStart: stateAfterStart,
47
+ ...completedWorkflowId !== void 0 && { completedWorkflowId }
48
+ });
49
+ return stateAfterStart;
50
+ }
@@ -1,6 +1,15 @@
1
1
  import type { ScheduleState } from '../types.ts';
2
2
  import type { EngineInternals } from './internals.ts';
3
3
  import type { ScheduleCallbacks } from './schedules.ts';
4
+ export type ScheduledRunStartOptions = {
5
+ occurrence?: number;
6
+ /** Reserved queue identity. A fresh id is minted when omitted. */
7
+ workflowId?: string;
8
+ /** Schedule state committed atomically with the workflow start. */
9
+ scheduleStateAfterStart?: ScheduleState;
10
+ /** Prior terminal run whose transient schedule metadata is now settled. */
11
+ completedWorkflowId?: string;
12
+ };
4
13
  /**
5
14
  * Launch one scheduled occurrence's workflow run and emit `schedule:fired`.
6
15
  *
@@ -11,7 +20,6 @@ import type { ScheduleCallbacks } from './schedules.ts';
11
20
  * active) never reach here, so they correctly do not fire.
12
21
  *
13
22
  * `occurrence` is the scheduled grid timestamp the run was due, threaded from the
14
- * timer loop. It is `undefined` for a `queue`-drained run, whose grid slot was
15
- * tracked only as a count and whose original timestamp is therefore gone.
23
+ * timer loop or retained on its durable queue entry until drain.
16
24
  */
17
- export declare function startScheduledRun(internals: EngineInternals, state: ScheduleState, callbacks: Pick<ScheduleCallbacks, 'startWorkflow' | 'failWorkflow' | 'handleCleanupError'>, occurrence?: number): Promise<string>;
25
+ export declare function startScheduledRun(internals: EngineInternals, state: ScheduleState, callbacks: Pick<ScheduleCallbacks, 'startWorkflow' | 'failWorkflow' | 'handleCleanupError'>, options?: ScheduledRunStartOptions): Promise<string>;
@@ -1,21 +1,44 @@
1
1
  import { KEYS } from "../../storage/interface.js";
2
+ import { encode } from "../codec.js";
2
3
  import { ScheduleFiredEvent } from "../events.js";
3
4
  import { unavailableServicesError } from "./lifecycle/recovered-services.js";
4
5
  import { EMPTY_STORAGE_VALUE } from "./lifecycle/shared.js";
5
6
  import { encodeScheduleRunMetadata } from "./schedule-run-metadata.js";
6
- export async function startScheduledRun(internals, state, callbacks, occurrence) {
7
- const workflowId = crypto.randomUUID(), scheduleRunOperations = [
7
+ export async function startScheduledRun(internals, state, callbacks, options = {}) {
8
+ const workflowId = options.workflowId ?? crypto.randomUUID(), occurrence = options.occurrence, metadata = encodeScheduleRunMetadata(state.id, occurrence), scheduleRunOperations = [
8
9
  {
9
10
  type: "put",
10
11
  key: KEYS.scheduleRun(workflowId),
11
- value: encodeScheduleRunMetadata(state.id, occurrence)
12
+ value: metadata
13
+ },
14
+ {
15
+ type: "put",
16
+ key: KEYS.scheduleRunLink(workflowId),
17
+ value: metadata
18
+ },
19
+ {
20
+ type: "put",
21
+ key: KEYS.scheduleRunBySchedule(state.id, workflowId),
22
+ value: EMPTY_STORAGE_VALUE
12
23
  },
13
24
  {
14
25
  type: "put",
15
26
  key: KEYS.terminalCleanupNeeded(workflowId),
16
27
  value: EMPTY_STORAGE_VALUE
17
28
  }
18
- ], resolution = await resolveScheduledRunServices(internals, workflowId, state, occurrence);
29
+ ];
30
+ if (options.scheduleStateAfterStart !== void 0)
31
+ scheduleRunOperations.push({
32
+ type: "put",
33
+ key: KEYS.schedule(options.scheduleStateAfterStart.id),
34
+ value: encode(options.scheduleStateAfterStart)
35
+ });
36
+ if (options.completedWorkflowId !== void 0)
37
+ scheduleRunOperations.push({
38
+ type: "delete",
39
+ key: KEYS.scheduleRun(options.completedWorkflowId)
40
+ });
41
+ const resolution = await resolveScheduledRunServices(internals, workflowId, state, occurrence);
19
42
  if (resolution !== null) {
20
43
  scheduleRunOperations.push({
21
44
  type: "put",
@@ -1,7 +1,11 @@
1
1
  import { ScheduleMissedFireEvent } from "../events/schedule-events.js";
2
2
  import { resolveEffectiveScheduleFireAt } from "./schedule-jitter.js";
3
3
  import { collectDueScheduleOccurrences, getNextScheduleOccurrence } from "./schedule-occurrence.js";
4
- import { loadScheduleState, writeScheduleState } from "./storage-io.js";
4
+ import {
5
+ loadScheduleState,
6
+ runSerializedScheduleStateOperation,
7
+ writeScheduleState
8
+ } from "./storage-io.js";
5
9
  const SCHEDULE_LATE_GRACE_MILLISECONDS = 1000, MAX_SCHEDULE_BACKFILL_OCCURRENCES_PER_TICK = 256, MAX_MISSED_FIRE_COUNT_PER_TICK = MAX_SCHEDULE_BACKFILL_OCCURRENCES_PER_TICK;
6
10
 
7
11
  class ScheduleStatePersistenceError extends Error {
@@ -12,6 +16,11 @@ class ScheduleStatePersistenceError extends Error {
12
16
  }
13
17
  }
14
18
  export async function handleScheduleTimer(internals, entry, callbacks) {
19
+ await runSerializedScheduleStateOperation(internals, entry.workflowId, async () => {
20
+ await handleSerializedScheduleTimer(internals, entry, callbacks);
21
+ });
22
+ }
23
+ async function handleSerializedScheduleTimer(internals, entry, callbacks) {
15
24
  if (internals.deposed)
16
25
  return;
17
26
  const state = await loadScheduleState(internals, entry.workflowId);
@@ -1,7 +1,8 @@
1
1
  import type { BatchOperation } from '../../storage/interface.ts';
2
- import type { PaginatedResult, ScheduleFilter, ScheduleOptions, ScheduleSpec, ScheduleState, ScheduleSummary, WorkflowState } from '../types.ts';
2
+ import type { PaginatedResult, ScheduleFilter, ScheduleOptions, ScheduleSpec, ScheduleState, ScheduleSummary, ScheduleUpdateOptions, WorkflowState } from '../types.ts';
3
3
  import type { EngineInternals } from './internals.ts';
4
4
  import { ScheduleHandle } from './schedule-handle.ts';
5
+ import type { ScheduledRunStartOptions } from './schedule-run.ts';
5
6
  export { startScheduledRun } from './schedule-run.ts';
6
7
  export { handleScheduleTimer } from './schedule-timer.ts';
7
8
  export type RefreshedScheduleState = {
@@ -16,7 +17,7 @@ export type ScheduleCallbacks = {
16
17
  cancelWorkflow: (workflowId: string) => Promise<void>;
17
18
  getWorkflowResult: (workflowId: string) => Promise<unknown>;
18
19
  refreshScheduledWorkflowState: (state: ScheduleState) => Promise<RefreshedScheduleState>;
19
- startScheduledRun: (state: ScheduleState, occurrence?: number) => Promise<string>;
20
+ startScheduledRun: (state: ScheduleState, options?: ScheduledRunStartOptions) => Promise<string>;
20
21
  applyScheduleOccurrence: (state: ScheduleState, occurrence?: number) => Promise<ScheduleState>;
21
22
  settleBackfillScheduleState: (state: ScheduleState) => Promise<ScheduleState>;
22
23
  flushQueuedInlineWorkflowStartsDirectly: () => Promise<void>;
@@ -30,8 +31,8 @@ export declare function toScheduleSummary(state: ScheduleState): ScheduleSummary
30
31
  export declare function pauseSchedule(internals: EngineInternals, scheduleId: string): Promise<void>;
31
32
  export declare function resumeSchedule(internals: EngineInternals, scheduleId: string): Promise<void>;
32
33
  export declare function cancelSchedule(internals: EngineInternals, scheduleId: string): Promise<void>;
33
- export declare function updateSchedule(internals: EngineInternals, scheduleId: string, newSpec: string | ScheduleSpec): Promise<void>;
34
+ export declare function updateSchedule(internals: EngineInternals, scheduleId: string, newSpec: string | ScheduleSpec, options?: ScheduleUpdateOptions): Promise<void>;
34
35
  export declare function refreshScheduledWorkflowState(internals: EngineInternals, state: ScheduleState, callbacks: Pick<ScheduleCallbacks, 'loadWorkflowState'>): Promise<RefreshedScheduleState>;
35
- export declare function applyScheduleOccurrence(_internals: EngineInternals, state: ScheduleState, callbacks: ScheduleCallbacks, occurrence?: number): Promise<ScheduleState>;
36
+ export declare function applyScheduleOccurrence(internals: EngineInternals, state: ScheduleState, callbacks: ScheduleCallbacks, occurrence?: number): Promise<ScheduleState>;
36
37
  export declare function settleBackfillScheduleState(internals: EngineInternals, state: ScheduleState, callbacks: Pick<ScheduleCallbacks, 'flushQueuedInlineWorkflowStartsDirectly' | 'refreshScheduledWorkflowState'>): Promise<ScheduleState>;
37
38
  export declare function handleScheduledWorkflowTerminal(internals: EngineInternals, workflowId: string, callbacks: ScheduleCallbacks): Promise<void>;
@@ -1,9 +1,11 @@
1
1
  import { KEYS } from "../../storage/interface.js";
2
2
  import { decode } from "../codec.js";
3
3
  import { WorkflowNotRegisteredError } from "./errors.js";
4
+ import { commitFencedEngineWrite } from "./fenced-write.js";
4
5
  import { ScheduleHandle } from "./schedule-handle.js";
5
6
  import { resolveEffectiveScheduleFireAt } from "./schedule-jitter.js";
6
7
  import { getNextScheduleOccurrence } from "./schedule-occurrence.js";
8
+ import { applyBlockedScheduleOccurrence, drainQueuedScheduleRun } from "./schedule-overlap.js";
7
9
  import { decodeScheduleRunMetadata } from "./schedule-run-metadata.js";
8
10
  import {
9
11
  clearScheduleCurrentWorkflow,
@@ -11,14 +13,20 @@ import {
11
13
  matchesScheduleFilter,
12
14
  paginateScheduleSummaries
13
15
  } from "./state-utilities.js";
14
- import { loadScheduleState, requireScheduleState, writeScheduleState } from "./storage-io.js";
16
+ import {
17
+ loadScheduleState,
18
+ requireScheduleState,
19
+ runSerializedScheduleStateOperation,
20
+ writeScheduleState
21
+ } from "./storage-io.js";
15
22
  import {
16
23
  coerceScheduleId,
17
24
  decodeScheduleState,
18
25
  isValidScheduleIdentifier,
19
26
  normalizeScheduleFilter,
20
27
  normalizeScheduleOptions,
21
- normalizeScheduleSpec
28
+ normalizeScheduleSpec,
29
+ normalizeScheduleUpdateOptions
22
30
  } from "./validation/schedule.js";
23
31
  export { startScheduledRun } from "./schedule-run.js";
24
32
  export { handleScheduleTimer } from "./schedule-timer.js";
@@ -48,7 +56,7 @@ export async function schedule(internals, type, input, spec, options) {
48
56
  updatedAt: now,
49
57
  nextFireAt: getNextScheduleOccurrence({ ...cadenceFields, createdAt: now }, now),
50
58
  missedFireCount: 0,
51
- queuedRuns: 0
59
+ queuedRuns: []
52
60
  };
53
61
  await writeScheduleState(internals, state);
54
62
  return new ScheduleHandle(scheduleId, internals.engine);
@@ -64,8 +72,7 @@ export async function listSchedules(internals, filter) {
64
72
  const state = decodeScheduleState(value);
65
73
  if (!state || !matchesScheduleFilter(state, normalizedFilter))
66
74
  continue;
67
- const { input: _input, ...summary } = state;
68
- items.push(summary);
75
+ items.push(toScheduleSummary(state));
69
76
  }
70
77
  return paginateScheduleSummaries(items, normalizedFilter);
71
78
  }
@@ -93,7 +100,7 @@ async function hasCurrentScheduleTimer(internals, state) {
93
100
  }
94
101
  export function toScheduleSummary(state) {
95
102
  const { input: _input, ...summary } = state;
96
- return summary;
103
+ return { ...summary, queuedRuns: summary.queuedRuns.map((queuedRun) => ({ ...queuedRun })) };
97
104
  }
98
105
  export async function pauseSchedule(internals, scheduleId) {
99
106
  const normalizedScheduleId = coerceScheduleId(scheduleId, "scheduleId"), state = await requireScheduleState(internals, normalizedScheduleId);
@@ -105,7 +112,7 @@ export async function pauseSchedule(internals, scheduleId) {
105
112
  status: "paused",
106
113
  updatedAt: now,
107
114
  nextFireAt: getNextScheduleOccurrence(state, now),
108
- queuedRuns: 0
115
+ queuedRuns: []
109
116
  };
110
117
  await writeScheduleState(internals, updatedState, { includeTimer: !1 });
111
118
  }
@@ -132,23 +139,30 @@ export async function cancelSchedule(internals, scheduleId) {
132
139
  status: "cancelled",
133
140
  updatedAt: internals.options.getNow(),
134
141
  nextFireAt: null,
135
- queuedRuns: 0
142
+ queuedRuns: []
136
143
  };
137
144
  await writeScheduleState(internals, updatedState, { includeTimer: !1 });
138
145
  }
139
- export async function updateSchedule(internals, scheduleId, newSpec) {
140
- const normalizedScheduleId = coerceScheduleId(scheduleId, "scheduleId"), normalizedSpec = normalizeScheduleSpec(newSpec), state = await requireScheduleState(internals, normalizedScheduleId), now = internals.options.getNow(), {
141
- cronExpression: _droppedCron,
142
- intervalMs: _droppedInterval,
143
- ...stateWithoutCadence
144
- } = state, cadenceFields = normalizedSpec.kind === "interval" ? { intervalMs: normalizedSpec.intervalMs } : { cronExpression: normalizedSpec.cronExpression }, anchorFields = normalizedSpec.kind === "interval" ? { createdAt: now } : {}, updatedState = {
145
- ...stateWithoutCadence,
146
- ...cadenceFields,
147
- ...anchorFields,
148
- updatedAt: now,
149
- nextFireAt: state.status === "cancelled" ? null : getNextScheduleOccurrence({ ...cadenceFields, createdAt: now }, now)
150
- };
151
- await writeScheduleState(internals, updatedState, { includeTimer: state.status === "active" });
146
+ export async function updateSchedule(internals, scheduleId, newSpec, options) {
147
+ const normalizedScheduleId = coerceScheduleId(scheduleId, "scheduleId"), normalizedSpec = normalizeScheduleSpec(newSpec), normalizedOptions = normalizeScheduleUpdateOptions(options);
148
+ await runSerializedScheduleStateOperation(internals, normalizedScheduleId, async () => {
149
+ const state = await requireScheduleState(internals, normalizedScheduleId), now = internals.options.getNow(), {
150
+ cronExpression: _droppedCron,
151
+ intervalMs: _droppedInterval,
152
+ ...stateWithoutCadence
153
+ } = state, cadenceFields = normalizedSpec.kind === "interval" ? { intervalMs: normalizedSpec.intervalMs } : { cronExpression: normalizedSpec.cronExpression }, anchorFields = normalizedSpec.kind === "interval" ? { createdAt: now } : {}, updatedState = {
154
+ ...stateWithoutCadence,
155
+ ...normalizedOptions,
156
+ ...cadenceFields,
157
+ ...anchorFields,
158
+ updatedAt: now,
159
+ nextFireAt: state.status === "cancelled" ? null : getNextScheduleOccurrence({ ...cadenceFields, createdAt: now }, now)
160
+ };
161
+ await writeScheduleState(internals, updatedState, {
162
+ includeTimer: state.status === "active",
163
+ replaceTimerFrom: state
164
+ });
165
+ });
152
166
  }
153
167
  function scheduledRunOccupiesSlot(currentWorkflowState) {
154
168
  const status = currentWorkflowState?.status;
@@ -169,31 +183,20 @@ export async function refreshScheduledWorkflowState(internals, state, callbacks)
169
183
  function hasActiveScheduledWorkflow(currentWorkflowState) {
170
184
  return scheduledRunOccupiesSlot(currentWorkflowState);
171
185
  }
172
- async function applyBlockedScheduleOccurrence(state, hasActiveWorkflow, callbacks, occurrence) {
173
- if (!hasActiveWorkflow)
174
- return { ...state, currentWorkflowId: await callbacks.startScheduledRun(state, occurrence) };
175
- if (state.overlap === "cancel-running") {
176
- if (state.currentWorkflowId) {
177
- callbacks.getWorkflowResult(state.currentWorkflowId).catch(() => {});
178
- await callbacks.cancelWorkflow(state.currentWorkflowId);
179
- }
180
- const stateForStart = clearScheduleCurrentWorkflow(state);
181
- return {
182
- ...state,
183
- currentWorkflowId: await callbacks.startScheduledRun(stateForStart, occurrence)
184
- };
186
+ export async function applyScheduleOccurrence(internals, state, callbacks, occurrence) {
187
+ const { state: refreshedState, currentWorkflowState } = await callbacks.refreshScheduledWorkflowState(state);
188
+ let stateForOccurrence = refreshedState, hasActiveWorkflow = hasActiveScheduledWorkflow(currentWorkflowState);
189
+ if (!hasActiveWorkflow && stateForOccurrence.queuedRuns.length > 0) {
190
+ stateForOccurrence = await drainQueuedScheduleRun(stateForOccurrence, callbacks);
191
+ hasActiveWorkflow = !0;
185
192
  }
186
- if (state.overlap === "queue")
187
- return { ...state, queuedRuns: state.queuedRuns + 1 };
188
- return state;
189
- }
190
- export async function applyScheduleOccurrence(_internals, state, callbacks, occurrence) {
191
- const { state: refreshedState, currentWorkflowState } = await callbacks.refreshScheduledWorkflowState(state), hasActiveWorkflow = hasActiveScheduledWorkflow(currentWorkflowState);
192
- if (refreshedState.overlap === "allow") {
193
- await callbacks.startScheduledRun(refreshedState, occurrence);
194
- return refreshedState;
193
+ if (stateForOccurrence.overlap === "allow") {
194
+ await callbacks.startScheduledRun(stateForOccurrence, {
195
+ ...occurrence !== void 0 && { occurrence }
196
+ });
197
+ return stateForOccurrence;
195
198
  }
196
- return applyBlockedScheduleOccurrence(refreshedState, hasActiveWorkflow, callbacks, occurrence);
199
+ return applyBlockedScheduleOccurrence(internals, stateForOccurrence, hasActiveWorkflow, callbacks, occurrence);
197
200
  }
198
201
  export async function settleBackfillScheduleState(internals, state, callbacks) {
199
202
  if (!state.currentWorkflowId)
@@ -208,26 +211,29 @@ export async function handleScheduledWorkflowTerminal(internals, workflowId, cal
208
211
  const scheduleRunBytes = await internals.storage.get(KEYS.scheduleRun(workflowId));
209
212
  if (!scheduleRunBytes)
210
213
  return;
211
- await internals.storage.delete(KEYS.scheduleRun(workflowId));
212
214
  const metadata = decodeScheduleRunMetadata(scheduleRunBytes);
213
- if (metadata === null || !isValidScheduleIdentifier(metadata.id))
215
+ if (metadata === null || !isValidScheduleIdentifier(metadata.id)) {
216
+ await deleteTransientScheduleRunMetadata(internals, workflowId);
214
217
  return;
218
+ }
215
219
  const scheduleId = metadata.id, state = await loadScheduleState(internals, scheduleId);
216
- if (!state || state.currentWorkflowId !== workflowId)
220
+ if (!state || state.currentWorkflowId !== workflowId) {
221
+ await deleteTransientScheduleRunMetadata(internals, workflowId);
217
222
  return;
223
+ }
218
224
  const now = internals.options.getNow(), clearedState = {
219
225
  ...clearScheduleCurrentWorkflow(state),
220
226
  updatedAt: now
221
227
  };
222
- if (clearedState.status === "active" && clearedState.overlap === "queue" && clearedState.queuedRuns > 0) {
223
- const nextWorkflowId = await callbacks.startScheduledRun(clearedState);
224
- await writeScheduleState(internals, {
225
- ...clearedState,
226
- currentWorkflowId: nextWorkflowId,
227
- queuedRuns: clearedState.queuedRuns - 1,
228
- updatedAt: now
229
- }, { includeTimer: !1 });
228
+ if (clearedState.status === "active" && clearedState.queuedRuns.length > 0) {
229
+ await drainQueuedScheduleRun({ ...clearedState, updatedAt: now }, callbacks, workflowId);
230
230
  return;
231
231
  }
232
- await writeScheduleState(internals, clearedState, { includeTimer: !1 });
232
+ await writeScheduleState(internals, clearedState, {
233
+ includeTimer: !1,
234
+ additionalOperations: [{ type: "delete", key: KEYS.scheduleRun(workflowId) }]
235
+ });
236
+ }
237
+ async function deleteTransientScheduleRunMetadata(internals, workflowId) {
238
+ await commitFencedEngineWrite(internals, [{ type: "delete", key: KEYS.scheduleRun(workflowId) }], [], () => Error(`Schedule-run cleanup for workflow "${workflowId}" lost its precondition.`));
233
239
  }
@@ -223,7 +223,7 @@ function matchesListFilterStatus(state, filter) {
223
223
  return (Array.isArray(filter.status) ? filter.status : [filter.status]).includes(state.status);
224
224
  }
225
225
  function matchesListFilterIdentity(state, filter) {
226
- return matchesListFilterStatus(state, filter) && (filter.type === void 0 || state.type === filter.type) && (filter.idPrefix === void 0 || state.id.startsWith(filter.idPrefix));
226
+ return matchesListFilterStatus(state, filter) && (filter.type === void 0 || state.type === filter.type) && (filter.parentWorkflowId === void 0 || state.parentWorkflowId === filter.parentWorkflowId) && (filter.parentWorkflowExecutionToken === void 0 || state.parentWorkflowExecutionToken === filter.parentWorkflowExecutionToken) && (filter.idPrefix === void 0 || state.id.startsWith(filter.idPrefix));
227
227
  }
228
228
  function matchesListFilterTimeRanges(state, filter) {
229
229
  if (filter.createdAt !== void 0 && !timestampInRange(state.createdAt, filter.createdAt))
@@ -3,6 +3,16 @@ import type { ScheduleState, WorkflowState } from '../types.ts';
3
3
  import type { EngineInternals } from './internals.ts';
4
4
  /** Run a workflow-state write after any earlier write for that workflow has settled. */
5
5
  export declare function runSerializedWorkflowStateWrite<TResult>(internals: EngineInternals, workflowId: string, writeOperation: () => Promise<TResult>): Promise<TResult>;
6
+ /**
7
+ * Serialize a schedule read-modify-write operation with its timer callback.
8
+ *
9
+ * Unlike workflow-state write serialization, a failed predecessor is propagated
10
+ * to callers that were already queued behind it. In particular, an update that
11
+ * arrives while a scanned timer callback is rearming must not delete the fired
12
+ * timer after that callback fails; rejecting the queued update leaves the durable
13
+ * timer available for the scheduler (or a lease successor) to retry.
14
+ */
15
+ export declare function runSerializedScheduleStateOperation<TResult>(internals: EngineInternals, scheduleId: string, operation: () => Promise<TResult>): Promise<TResult>;
6
16
  /** Load and decode persisted workflow state by workflow ID. */
7
17
  export declare function loadWorkflowState(internals: EngineInternals, workflowId: string): Promise<WorkflowState | null>;
8
18
  /** Load a terminal workflow result or throw the persisted terminal error. */
@@ -29,6 +39,8 @@ export declare function requireScheduleState(internals: EngineInternals, schedul
29
39
  /** Persist schedule state and optionally write the next schedule timer. */
30
40
  export declare function writeScheduleState(internals: EngineInternals, state: ScheduleState, options?: {
31
41
  includeTimer?: boolean;
42
+ replaceTimerFrom?: ScheduleState;
43
+ additionalOperations?: BatchOperation[];
32
44
  }): Promise<void>;
33
45
  /** Load persisted workflow start headers by workflow ID. */
34
46
  export declare function loadWorkflowStartHeaders(internals: EngineInternals, workflowId: string): Promise<Map<string, string> | undefined>;
@@ -30,6 +30,21 @@ export async function runSerializedWorkflowStateWrite(internals, workflowId, wri
30
30
  internals.workflowStateWriteChains.delete(workflowId);
31
31
  }
32
32
  }
33
+ export async function runSerializedScheduleStateOperation(internals, scheduleId, operation) {
34
+ const execution = (internals.scheduleStateOperationChains.get(scheduleId) ?? Promise.resolve()).then(operation), trackedExecution = execution.then(() => {
35
+ return;
36
+ });
37
+ trackedExecution.catch(() => {
38
+ return;
39
+ });
40
+ internals.scheduleStateOperationChains.set(scheduleId, trackedExecution);
41
+ try {
42
+ return await execution;
43
+ } finally {
44
+ if (internals.scheduleStateOperationChains.get(scheduleId) === trackedExecution)
45
+ internals.scheduleStateOperationChains.delete(scheduleId);
46
+ }
47
+ }
33
48
  export async function loadWorkflowState(internals, workflowId) {
34
49
  const bytes = await internals.storage.get(KEYS.workflow(workflowId));
35
50
  if (!bytes)
@@ -80,19 +95,43 @@ export async function requireScheduleState(internals, scheduleId) {
80
95
  throw Error(`Schedule "${scheduleId}" not found`);
81
96
  return state;
82
97
  }
98
+ function buildPreviousScheduleTimerDeleteOperations(previousState) {
99
+ if (previousState?.status !== "active" || previousState.nextFireAt === null)
100
+ return [];
101
+ return [
102
+ {
103
+ type: "delete",
104
+ key: KEYS.scheduleTick(resolveEffectiveScheduleFireAt(previousState, previousState.nextFireAt), previousState.id)
105
+ }
106
+ ];
107
+ }
108
+ function buildNextScheduleTimerOperations(state, includeTimer) {
109
+ if (!includeTimer || state.status !== "active" || state.nextFireAt === null)
110
+ return [];
111
+ return buildTimerBatchOperations({
112
+ id: createScheduleTimerId(state.id),
113
+ workflowId: state.id,
114
+ fireAt: resolveEffectiveScheduleFireAt(state, state.nextFireAt),
115
+ kind: "schedule"
116
+ });
117
+ }
118
+ function buildScheduleTimerReplacementOperations(state, includeTimer, previousState) {
119
+ const deleteOperations = buildPreviousScheduleTimerDeleteOperations(previousState), nextTimerOperations = buildNextScheduleTimerOperations(state, includeTimer);
120
+ if (nextTimerOperations.length > 0)
121
+ return [...deleteOperations, ...nextTimerOperations];
122
+ if (previousState === void 0)
123
+ return deleteOperations;
124
+ return [
125
+ ...deleteOperations,
126
+ { type: "delete", key: `timer-idx:${createScheduleTimerId(state.id)}` }
127
+ ];
128
+ }
83
129
  export async function writeScheduleState(internals, state, options) {
84
130
  const operations = [
85
131
  { type: "put", key: KEYS.schedule(state.id), value: encode(state) }
86
- ];
87
- if ((options?.includeTimer ?? state.status === "active") && state.status === "active" && state.nextFireAt !== null) {
88
- const effectiveFireAt = resolveEffectiveScheduleFireAt(state, state.nextFireAt);
89
- operations.push(...buildTimerBatchOperations({
90
- id: createScheduleTimerId(state.id),
91
- workflowId: state.id,
92
- fireAt: effectiveFireAt,
93
- kind: "schedule"
94
- }));
95
- }
132
+ ], includeTimer = options?.includeTimer ?? state.status === "active";
133
+ operations.push(...buildScheduleTimerReplacementOperations(state, includeTimer, options?.replaceTimerFrom));
134
+ operations.push(...options?.additionalOperations ?? []);
96
135
  await commitFencedEngineWrite(internals, operations, [], () => Error(`Schedule state commit for schedule "${state.id}" lost its precondition.`));
97
136
  }
98
137
  export async function loadWorkflowStartHeaders(internals, workflowId) {
@@ -37,11 +37,11 @@ export declare function runningClaimIsStale(internals: EngineInternals, claim: T
37
37
  /**
38
38
  * Durable audit record written to {@link KEYS.teardownDeadLetter} when a workflow's
39
39
  * finalizer permanently fails — the retry horizon was reached, or the recorded resource
40
- * state vanished so the finalizer can never run. It is the supported operator surface
41
- * for a leaked external resource: raw storage inspection of the `wf-teardown-deadletter:`
42
- * key is the contract (in-process teardown events are best-effort and not durable, and
43
- * the record is excluded from the workflow purge delete-set so it survives after the
44
- * workflow record is gone).
40
+ * state vanished so the finalizer can never run. It is the supported durable operator
41
+ * record for a leaked external resource; {@link Engine.getFinalizerStatus} and the matching
42
+ * transport operation expose it without relying on best-effort teardown events. The record
43
+ * is excluded from the workflow purge delete-set so it survives after the workflow record
44
+ * is gone.
45
45
  */
46
46
  export interface TeardownDeadLetterRecord {
47
47
  /** The workflow type whose finalizer leaked. */
@@ -52,6 +52,8 @@ export interface TeardownDeadLetterRecord {
52
52
  attempts: number;
53
53
  /** Engine clock when the record was written. */
54
54
  deadLetteredAt: number;
55
+ /** Concrete run identity, present on records written from current workflow state. */
56
+ workflowExecutionToken?: string;
55
57
  /** The decoded `ctx.setFinalizerState` payload, when it was still recoverable. */
56
58
  finalizerInput?: unknown;
57
59
  }
@@ -107,4 +109,4 @@ export declare function settleOnRunningClaim(internals: EngineInternals, workflo
107
109
  export declare function deadLetterTeardown(internals: EngineInternals, workflowId: string, workflowType: string, attempts: number, expectedBytes: Uint8Array, details: {
108
110
  lastError: string;
109
111
  finalizerInput: unknown;
110
- }): Promise<boolean>;
112
+ }, workflowExecutionToken?: string): Promise<boolean>;