@lostgradient/weft 0.20.0 → 0.21.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 (138) hide show
  1. package/README.md +2 -2
  2. package/dist/core/engine/activity-reconciliation.d.ts +2 -2
  3. package/dist/core/engine/activity-reconciliation.js +12 -8
  4. package/dist/core/engine/async-activity-completion.js +12 -4
  5. package/dist/core/engine/async-activity-records.d.ts +11 -1
  6. package/dist/core/engine/async-activity-records.js +4 -3
  7. package/dist/core/engine/attributes-tags.d.ts +11 -1
  8. package/dist/core/engine/attributes-tags.js +4 -3
  9. package/dist/core/engine/bulk-operations-purge.js +5 -2
  10. package/dist/core/engine/bulk-operations.js +17 -5
  11. package/dist/core/engine/callback-creators-bundles.js +1 -1
  12. package/dist/core/engine/callback-creators-core.js +4 -2
  13. package/dist/core/engine/checkpoint-io.js +1 -1
  14. package/dist/core/engine/child-workflow.js +2 -1
  15. package/dist/core/engine/completed-review-storage.js +1 -1
  16. package/dist/core/engine/condition-waiters.d.ts +45 -3
  17. package/dist/core/engine/condition-waiters.js +12 -3
  18. package/dist/core/engine/disposal.js +6 -0
  19. package/dist/core/engine/engine-internal-types.d.ts +12 -4
  20. package/dist/core/engine/fenced-write.d.ts +46 -15
  21. package/dist/core/engine/fenced-write.js +39 -6
  22. package/dist/core/engine/finalizer-state.d.ts +3 -1
  23. package/dist/core/engine/handle-result.d.ts +49 -1
  24. package/dist/core/engine/handle-result.js +95 -20
  25. package/dist/core/engine/index.d.ts +13 -5
  26. package/dist/core/engine/index.js +155 -5
  27. package/dist/core/engine/inline-launch-queue.js +9 -0
  28. package/dist/core/engine/internals.d.ts +73 -0
  29. package/dist/core/engine/invoke-update-handler.d.ts +19 -0
  30. package/dist/core/engine/invoke-update-handler.js +7 -0
  31. package/dist/core/engine/lease-deposition.d.ts +132 -0
  32. package/dist/core/engine/lease-deposition.js +31 -0
  33. package/dist/core/engine/lease-errors.d.ts +80 -9
  34. package/dist/core/engine/lease-errors.js +28 -2
  35. package/dist/core/engine/lifecycle/resume.js +3 -1
  36. package/dist/core/engine/lifecycle/standalone-claim-acquire.d.ts +33 -0
  37. package/dist/core/engine/lifecycle/standalone-claim-acquire.js +25 -0
  38. package/dist/core/engine/lifecycle/start-commit.js +20 -6
  39. package/dist/core/engine/lifecycle/transition.d.ts +15 -1
  40. package/dist/core/engine/lifecycle/transition.js +6 -3
  41. package/dist/core/engine/operations-activity.js +1 -1
  42. package/dist/core/engine/operations-time.js +21 -7
  43. package/dist/core/engine/owner-side-signal-poll.d.ts +132 -0
  44. package/dist/core/engine/owner-side-signal-poll.js +22 -0
  45. package/dist/core/engine/owner-side-update-poll.d.ts +116 -0
  46. package/dist/core/engine/owner-side-update-poll.js +22 -0
  47. package/dist/core/engine/ownership-bootstrap.d.ts +280 -0
  48. package/dist/core/engine/ownership-bootstrap.js +89 -0
  49. package/dist/core/engine/ownership-mode-marker.d.ts +82 -0
  50. package/dist/core/engine/ownership-mode-marker.js +52 -0
  51. package/dist/core/engine/ownership-options.d.ts +22 -6
  52. package/dist/core/engine/ownership-options.js +42 -12
  53. package/dist/core/engine/pending-updates.js +35 -24
  54. package/dist/core/engine/queries.d.ts +70 -0
  55. package/dist/core/engine/queries.js +58 -4
  56. package/dist/core/engine/schedules.js +1 -1
  57. package/dist/core/engine/signals.d.ts +11 -1
  58. package/dist/core/engine/signals.js +11 -7
  59. package/dist/core/engine/sleep-timer-acknowledgements.d.ts +68 -0
  60. package/dist/core/engine/sleep-timer-acknowledgements.js +19 -0
  61. package/dist/core/engine/storage-io.d.ts +58 -10
  62. package/dist/core/engine/storage-io.js +27 -9
  63. package/dist/core/engine/termination/cleanup.d.ts +5 -4
  64. package/dist/core/engine/termination/complete.js +23 -4
  65. package/dist/core/engine/termination/finalizer-claim.js +4 -4
  66. package/dist/core/engine/termination/state-commit-callbacks.d.ts +15 -0
  67. package/dist/core/engine/termination/state-commit-callbacks.js +0 -0
  68. package/dist/core/engine/termination/suspend.js +1 -1
  69. package/dist/core/engine/update-validation.d.ts +28 -0
  70. package/dist/core/engine/update-validation.js +37 -0
  71. package/dist/core/engine/updates.d.ts +17 -16
  72. package/dist/core/engine/updates.js +24 -47
  73. package/dist/core/engine/wake-ownership-check.d.ts +86 -0
  74. package/dist/core/engine/wake-ownership-check.js +19 -0
  75. package/dist/core/engine/wake-ownership-guard.d.ts +46 -0
  76. package/dist/core/engine/wake-ownership-guard.js +23 -0
  77. package/dist/core/engine/workflow-claim-codec.d.ts +64 -0
  78. package/dist/core/engine/workflow-claim-codec.js +44 -0
  79. package/dist/core/engine/workflow-claim-cooldown.d.ts +60 -0
  80. package/dist/core/engine/workflow-claim-cooldown.js +17 -0
  81. package/dist/core/engine/workflow-claim-fold.d.ts +82 -0
  82. package/dist/core/engine/workflow-claim-fold.js +53 -0
  83. package/dist/core/engine/workflow-claim-metrics.d.ts +131 -0
  84. package/dist/core/engine/workflow-claim-metrics.js +62 -0
  85. package/dist/core/engine/workflow-claim-reclaim-scan.d.ts +96 -0
  86. package/dist/core/engine/workflow-claim-reclaim-scan.js +66 -0
  87. package/dist/core/engine/workflow-claim-reclaim-target.d.ts +107 -0
  88. package/dist/core/engine/workflow-claim-reclaim-target.js +155 -0
  89. package/dist/core/engine/workflow-claim-registry.d.ts +227 -0
  90. package/dist/core/engine/workflow-claim-registry.js +198 -0
  91. package/dist/core/engine/workflow-claim-renewal-interval.d.ts +50 -0
  92. package/dist/core/engine/workflow-claim-renewal-interval.js +132 -0
  93. package/dist/core/engine/workflow-claim-renewal-subpasses.d.ts +265 -0
  94. package/dist/core/engine/workflow-claim-renewal-subpasses.js +104 -0
  95. package/dist/core/engine/workflow-claim-renewal-task.d.ts +159 -0
  96. package/dist/core/engine/workflow-claim-renewal-task.js +65 -0
  97. package/dist/core/engine/workflow-claim-transitions.d.ts +186 -0
  98. package/dist/core/engine/workflow-claim-transitions.js +120 -0
  99. package/dist/core/inline-execution-strategy.d.ts +20 -0
  100. package/dist/core/inline-execution-strategy.js +15 -4
  101. package/dist/core/scheduler/scheduler-class.js +3 -3
  102. package/dist/core/scheduler/timer-sources.d.ts +13 -0
  103. package/dist/core/scheduler/timer-sources.js +1 -1
  104. package/dist/core/types/options.d.ts +17 -1
  105. package/dist/core/weft-error.d.ts +1 -1
  106. package/dist/core/weft-error.js +2 -1
  107. package/dist/http.js +2 -2
  108. package/dist/index.d.ts +2 -1
  109. package/dist/index.js +8 -0
  110. package/dist/indexeddb.js +1 -1
  111. package/dist/json-schema.js +3 -3
  112. package/dist/observability/index.js +2 -2
  113. package/dist/storage/auto.js +1 -1
  114. package/dist/storage/bun-sql.js +113 -0
  115. package/dist/storage/compressed-storage.js +1 -1
  116. package/dist/storage/index.d.ts +38 -35
  117. package/dist/storage/interface.d.ts +87 -125
  118. package/dist/storage/interface.js +1 -1
  119. package/dist/storage/key-encoding.d.ts +51 -0
  120. package/dist/storage/key-encoding.js +13 -0
  121. package/dist/storage/key-prefixes.d.ts +1 -1
  122. package/dist/storage/key-prefixes.js +3 -0
  123. package/dist/storage/lmdb.js +1 -1
  124. package/dist/storage/memory.js +1 -1
  125. package/dist/storage/neon.js +2 -2
  126. package/dist/storage/node-sqlite.js +113 -0
  127. package/dist/storage/ownership-keys.d.ts +55 -0
  128. package/dist/storage/ownership-keys.js +6 -0
  129. package/dist/storage/postgres.js +2 -2
  130. package/dist/storage/resolve.js +1 -1
  131. package/dist/storage/scoped-storage.js +1 -1
  132. package/dist/storage/testing.js +1 -1
  133. package/dist/storage/turso.js +2 -2
  134. package/dist/version.d.ts +1 -1
  135. package/dist/version.js +1 -1
  136. package/dist/web-extension.js +1 -1
  137. package/dist/worker/protocol.js +1 -1
  138. package/package.json +1 -1
@@ -11,6 +11,7 @@ import { validateSignalId } from "../signal-id.js";
11
11
  import { commitAnonymousSignalOperations } from "./anonymous-signal-sequence.js";
12
12
  import { stageAtomicWorkflowCommitSideEffects } from "./checkpoint-side-effects.js";
13
13
  import { isTerminalWorkflowStatus } from "./validation.js";
14
+ import { confirmWakeOwnership } from "./wake-ownership-guard.js";
14
15
  const EMPTY_STORAGE_VALUE = new Uint8Array(0), SIGNAL_ACCEPTED_RESPONSE = { ok: !0 }, SIGNAL_KEY_COMPONENT_COUNT = 5;
15
16
  export async function signal(internals, workflowId, name, payload, callbacks, options = {}) {
16
17
  const deliverSignal = async (targetWorkflowId, signalName, signalPayload) => {
@@ -44,11 +45,12 @@ export async function signal(internals, workflowId, name, payload, callbacks, op
44
45
  export function releaseSignalWaiter(internals, workflowId, waiterKey, expectedResolve) {
45
46
  const currentWaiter = internals.signalWaiters.get(waiterKey);
46
47
  if (!currentWaiter)
47
- return;
48
+ return !1;
48
49
  if (expectedResolve && currentWaiter !== expectedResolve)
49
- return;
50
+ return !1;
50
51
  internals.signalWaiters.delete(waiterKey);
51
52
  untrackWaiterKey(internals.signalWaitersByWorkflow, workflowId, waiterKey);
53
+ return !0;
52
54
  }
53
55
  export async function bufferSignalPayloads(internals, workflowId, deliveries, callbacks, defaultOptions = {}) {
54
56
  if (deliveries.length === 0)
@@ -69,11 +71,11 @@ export async function bufferSignalPayloads(internals, workflowId, deliveries, ca
69
71
  if (!await storageConditionalBatch(internals.storage, [{ key: acceptedResponseKey, expectedValue: null }], [...operations, { type: "put", key: acceptedResponseKey, value: acceptedResponse }]))
70
72
  return;
71
73
  markTerminalCleanupTracked(internals, workflowId);
72
- deliverBufferedSignals(internals, workflowId, deliveries, callbacks);
74
+ await deliverBufferedSignals(internals, workflowId, deliveries, callbacks);
73
75
  return;
74
76
  }
75
77
  await commitAnonymousSignalOperations(internals, workflowId, deliveries, (operations) => appendTerminalCleanupOperation(internals, workflowId, operations), () => markTerminalCleanupTracked(internals, workflowId));
76
- deliverBufferedSignals(internals, workflowId, deliveries, callbacks);
78
+ await deliverBufferedSignals(internals, workflowId, deliveries, callbacks);
77
79
  }
78
80
  function createExplicitSignalOperations(internals, workflowId, deliveries, signalId) {
79
81
  return deliveries.map(({ signalName, payload }) => ({
@@ -109,7 +111,7 @@ function appendTerminalCleanupOperation(internals, workflowId, operations) {
109
111
  function markTerminalCleanupTracked(internals, workflowId) {
110
112
  internals.workflowsNeedingTerminalCleanup.add(workflowId);
111
113
  }
112
- function deliverBufferedSignals(internals, workflowId, deliveries, callbacks) {
114
+ async function deliverBufferedSignals(internals, workflowId, deliveries, callbacks) {
113
115
  let shouldResumeParkedWorkflow = !1;
114
116
  for (const { signalName, payload, options } of deliveries) {
115
117
  if (options?.emitPublicEvent !== !1) {
@@ -118,8 +120,10 @@ function deliverBufferedSignals(internals, workflowId, deliveries, callbacks) {
118
120
  }
119
121
  const waiterKey = `${workflowId}:${signalName}`, waiter = internals.signalWaiters.get(waiterKey);
120
122
  if (waiter) {
121
- releaseSignalWaiter(internals, workflowId, waiterKey, waiter);
122
- waiter();
123
+ if (internals.workflowClaimRegistry !== null && await confirmWakeOwnership(internals, workflowId, "signal") === "discard")
124
+ continue;
125
+ if (releaseSignalWaiter(internals, workflowId, waiterKey, waiter))
126
+ waiter();
123
127
  continue;
124
128
  }
125
129
  if (internals.parkedInlineWorkflows.has(workflowId))
@@ -5,7 +5,75 @@ export type SleepTimerAcknowledgement = {
5
5
  cancel: () => void;
6
6
  promise: Promise<void>;
7
7
  };
8
+ /**
9
+ * Durable sleep timers fire globally — under `ownership: 'workflow-lease'`
10
+ * every engine sharing the store observes the same expired timer, not only
11
+ * the workflow's owner (see ADR 0002's entry-point classification: the
12
+ * scheduler dispatch shell is claim-acquiring only for claim-ACQUIRING
13
+ * branches; this one is claim-REQUIRING and checks for itself). Running
14
+ * `confirmWakeOwnership` FIRST — before `shouldIgnoreUnclaimedSleepTimer`
15
+ * even loads workflow state — matters: on a non-owning engine that state
16
+ * legitimately reads `'running'` (the true owner is actively driving it),
17
+ * which would otherwise hit `shouldIgnoreUnclaimedSleepTimer`'s "fired
18
+ * before ready" throw meant for a same-engine registration race, not a
19
+ * cross-engine ownership miss.
20
+ *
21
+ * A discard is NOT a silent no-op: `handleTimerFired`'s caller — the
22
+ * `Scheduler` — treats a callback that returns without throwing as
23
+ * "processed" and durably deletes the fired timer key
24
+ * (`commitTimerCleanup`, engine-scoped and unfenced on any single workflow's
25
+ * claim, since one tick's cleanup batch can span fired timers from many
26
+ * workflows — see `src/core/engine/index.ts`'s `Scheduler` wiring). If a
27
+ * discarding non-owner let that deletion proceed, it would delete the true
28
+ * owner's only durable record of this fire before the owner ever observes
29
+ * it — see `retainDiscardedDurableTimer` below.
30
+ */
8
31
  export declare function handleSleepTimerWithAcknowledgement(internals: EngineInternals, entry: TimerEntry, loadWorkflowState: (workflowId: string) => Promise<WorkflowState | null>): Promise<void>;
32
+ /** What to do with a durable timer key once a claim-requiring wake has discarded it. */
33
+ export type DiscardedTimerDisposition = 'retain' | 'collect';
34
+ /**
35
+ * Decide what a discarded claim-requiring timer fire (ADR 0002's `sleep` and
36
+ * `wait-condition` wake kinds — see `operations-time.ts`'s
37
+ * `resolveConditionTimer` for the second caller) should do with its durable
38
+ * timer key. Shared by both wake kinds because they share the exact same
39
+ * hazard: `commitTimerCleanup` batches deletes across many workflows in one
40
+ * engine-scoped, unfenced write, so a non-owner that discards must not let
41
+ * the Scheduler treat the fire as "processed" while some other engine still
42
+ * needs the same durable record to perform the real wake.
43
+ *
44
+ * "Not locally owned" is not by itself proof the timer is stale, though —
45
+ * `resolveSleepTimer`'s own comment documents that a durable sleep timer
46
+ * OUTLIVES terminal cleanup (cleanup only drops the in-memory resolver, not
47
+ * the durable key). Blindly retaining on every discard would turn an
48
+ * orphaned post-terminal timer into an immortal one: every engine sharing
49
+ * the store would rediscover it, discard it, and retain it again on every
50
+ * Scheduler tick forever. So this reads the workflow's CURRENT persisted
51
+ * status to disambiguate:
52
+ * - `null`, or a terminal status ({@link isTerminalWorkflowStatus}):
53
+ * `'collect'` — no engine holds or will ever again acquire a claim for
54
+ * this workflow, so nothing will ever consume this fire; let the
55
+ * Scheduler's normal cleanup remove the orphaned key, matching
56
+ * pre-ADR-0002 behavior for an unclaimed timer.
57
+ * - `'suspended'`: `'collect'` — `engine.suspend()`'s durable re-arm
58
+ * establishes its own fresh timer on resume; the pre-suspend fire being
59
+ * discarded here is not the one that wakes the resumed run.
60
+ * - any other status (`'running'`, `'pending'`): `'retain'` — some engine
61
+ * still holds, or will still acquire, a live claim for this workflow;
62
+ * leave the durable key so that engine's own copy of this same fire
63
+ * performs the real wake and deletes it for real, bounded by that
64
+ * engine's own next Scheduler poll.
65
+ */
66
+ export declare function resolveDiscardedTimerDisposition(workflowId: string, loadWorkflowState: (workflowId: string) => Promise<WorkflowState | null>): Promise<DiscardedTimerDisposition>;
67
+ /**
68
+ * Apply {@link resolveDiscardedTimerDisposition} to a discarded claim-requiring
69
+ * timer fire: throws when the durable key must be retained, so the
70
+ * `Scheduler`'s `#processSelectedTimer` catch block treats this fire as
71
+ * `'retry'` — leaving the timer key in storage instead of collecting it —
72
+ * and resolves normally when the workflow is gone, terminal, or suspended,
73
+ * so a genuinely orphaned timer is still collected exactly as it was before
74
+ * this ownership check existed.
75
+ */
76
+ export declare function retainDiscardedDurableTimer(timerId: string, workflowId: string, loadWorkflowState: (workflowId: string) => Promise<WorkflowState | null>): Promise<void>;
9
77
  export declare function createSleepTimerAcknowledgement(internals: EngineInternals, workflowId: string, operationId: string, fireAt: number): SleepTimerAcknowledgement;
10
78
  export declare function recordDurableInlineOperation(internals: EngineInternals, workflowId: string, operation: ContextOperationRequest): void;
11
79
  export declare function settleSleepTimerAcknowledgements(internals: EngineInternals, workflowId: string, disposition: 'suspended' | 'terminal'): void;
@@ -1,4 +1,10 @@
1
+ import { isTerminalWorkflowStatus } from "./validation.js";
2
+ import { confirmWakeOwnership } from "./wake-ownership-guard.js";
1
3
  export async function handleSleepTimerWithAcknowledgement(internals, entry, loadWorkflowState) {
4
+ if (await confirmWakeOwnership(internals, entry.workflowId, "sleep") === "discard") {
5
+ await retainDiscardedDurableTimer(entry.id, entry.workflowId, loadWorkflowState);
6
+ return;
7
+ }
2
8
  const operationId = entry.id.replace("sleep:", "");
3
9
  if (await shouldIgnoreUnclaimedSleepTimer(internals, entry, operationId, loadWorkflowState))
4
10
  return;
@@ -9,6 +15,19 @@ export async function handleSleepTimerWithAcknowledgement(internals, entry, load
9
15
  }
10
16
  await acknowledgement?.promise;
11
17
  }
18
+ export async function resolveDiscardedTimerDisposition(workflowId, loadWorkflowState) {
19
+ const state = await loadWorkflowState(workflowId);
20
+ if (state === null)
21
+ return "collect";
22
+ if (state.status === "suspended" || isTerminalWorkflowStatus(state.status))
23
+ return "collect";
24
+ return "retain";
25
+ }
26
+ export async function retainDiscardedDurableTimer(timerId, workflowId, loadWorkflowState) {
27
+ if (await resolveDiscardedTimerDisposition(workflowId, loadWorkflowState) !== "retain")
28
+ return;
29
+ throw Error(`Durable timer "${timerId}" for workflow "${workflowId}" was discarded by a non-owning engine under ownership: 'workflow-lease'; retaining it in storage for the true owner instead of letting the scheduler delete it.`);
30
+ }
12
31
  async function shouldIgnoreUnclaimedSleepTimer(internals, entry, operationId, loadWorkflowState) {
13
32
  const resolverKey = `${entry.workflowId}:${operationId}`;
14
33
  if (internals.sleepResolvers.has(resolverKey) || internals.inlineStrategy === null)
@@ -1,6 +1,7 @@
1
1
  import { type BatchOperation } from '../../storage/interface.ts';
2
2
  import type { ScheduleState, WorkflowState } from '../types.ts';
3
3
  import type { EngineInternals } from './internals.ts';
4
+ import { type WorkflowClaimTransitionFragment } from './workflow-claim-transitions.ts';
4
5
  /** Run a workflow-state write after any earlier write for that workflow has settled. */
5
6
  export declare function runSerializedWorkflowStateWrite<TResult>(internals: EngineInternals, workflowId: string, writeOperation: () => Promise<TResult>): Promise<TResult>;
6
7
  /**
@@ -15,23 +16,70 @@ export declare function runSerializedWorkflowStateWrite<TResult>(internals: Engi
15
16
  export declare function runSerializedScheduleStateOperation<TResult>(internals: EngineInternals, scheduleId: string, operation: () => Promise<TResult>): Promise<TResult>;
16
17
  /** Load and decode persisted workflow state by workflow ID. */
17
18
  export declare function loadWorkflowState(internals: EngineInternals, workflowId: string): Promise<WorkflowState | null>;
19
+ /**
20
+ * Derive a terminal result (or throw the persisted terminal error) from an
21
+ * ALREADY-LOADED `WorkflowState` — pure, no storage read. Callers that hold a
22
+ * state snapshot they have already validated as terminal (e.g.
23
+ * `bootstrapWorkflowResultResolver`) must use this instead of
24
+ * {@link loadWorkflowResult}: a second independent `loadWorkflowState` read
25
+ * can observe a DIFFERENT run than the one the caller validated, if
26
+ * `onTerminalConflict: 'start-new'` replaces the workflow between the two
27
+ * reads — attributing a replacement run's result (or a spurious "still
28
+ * running") to a waiter that was made terminal by the original run.
29
+ */
30
+ export declare function deriveWorkflowResultFromState(state: WorkflowState): unknown;
18
31
  /** Load a terminal workflow result or throw the persisted terminal error. */
19
32
  export declare function loadWorkflowResult(internals: EngineInternals, workflowId: string): Promise<unknown>;
20
33
  type WorkflowStateCommitOptions = {
21
34
  includePendingAtomicSideEffects?: boolean;
22
35
  };
23
36
  /**
24
- * Commit an engine-generator-owned workflow-state advance, FENCED on the lease
25
- * epoch under `ownership: 'lease'` (issue #470 Step 2). A deposed engine's write
26
- * loses its CAS instead of corrupting the successor's state; the deposition is
27
- * detected and the engine halts (see {@link commitFencedEngineWrite}). Under
28
- * `ownership: 'none'` it is byte-for-byte the pre-Step-2 commit shape the epoch
29
- * condition is only appended when a lease is held. Use this for suspend,
30
- * completion, and other state advances driven by the workflow lifecycle. Operator/
31
- * external mutations (search-attribute and tag edits) do NOT use this helper; they
32
- * batch directly and are intentionally never fenced.
37
+ * Build the `wf-owner-epoch:<id>` / `wf-owner-holder:<id>` ROTATION fragment
38
+ * for an EXTERNAL terminal transition cancel, timeout, suspend, purge (ADR
39
+ * 0002 § "External terminal transitions must rotate the epoch"). Any engine
40
+ * may commit these against a workflow it does not own; deleting the holder
41
+ * alone is not sufficient to make that safe, so the epoch is rotated in the
42
+ * SAME atomic batch that writes the terminal/suspended state, deposing a
43
+ * still-running owner its next write carries the now-stale epoch and loses
44
+ * its CAS. Meant to be folded (via `[...fragment.operations]` /
45
+ * `[...fragment.conditions]`) into the caller's own operations/conditions,
46
+ * never committed standalone.
47
+ *
48
+ * Under `ownership: 'none'` or `'lease'` this returns an EMPTY fragment with
49
+ * NO storage read: the `wf-owner-*` keyspace is not in play under those
50
+ * modes, and this function must not touch it — that is what keeps external
51
+ * terminal commits byte-for-byte unchanged there.
52
+ */
53
+ export declare function buildExternalTerminalRotationFragment(internals: EngineInternals, workflowId: string): Promise<WorkflowClaimTransitionFragment>;
54
+ /**
55
+ * Commit a SELF-transition workflow-state advance (complete, fail): this
56
+ * engine is finishing its OWN workflow, so the write is workflow-scoped and
57
+ * fenced on THIS engine's claim epoch under `ownership: 'workflow-lease'`
58
+ * (via {@link commitFencedEngineWrite}'s `workflowId` parameter), or the
59
+ * global lease epoch under `ownership: 'lease'`. A deposed engine's write
60
+ * loses its CAS instead of corrupting the successor's state; the deposition
61
+ * is detected and the engine (or, under `workflow-lease`, just this one
62
+ * workflow) halts. Under `ownership: 'none'` this is byte-for-byte the
63
+ * pre-ADR commit shape. Never rotates `wf-owner-epoch:<id>` — that is the
64
+ * external-transition shape below. Operator/external mutations
65
+ * (search-attribute and tag edits) do NOT use this helper; they batch
66
+ * directly and are intentionally never fenced.
67
+ */
68
+ export declare function commitSelfWorkflowStateOperations(internals: EngineInternals, state: WorkflowState, operations: BatchOperation[], options?: WorkflowStateCommitOptions): Promise<void>;
69
+ /**
70
+ * Commit an EXTERNAL terminal workflow-state transition — cancel, timeout, or
71
+ * suspend (ADR 0002 § "External terminal transitions must rotate the
72
+ * epoch"). ANY engine may commit these against a workflow it does not own, so
73
+ * — unlike {@link commitSelfWorkflowStateOperations} — this is never fenced
74
+ * on this engine's own workflow claim (`workflowId: null` is passed to
75
+ * {@link commitFencedEngineWrite}). Instead {@link
76
+ * buildExternalTerminalRotationFragment} folds a claim ROTATION into the same
77
+ * atomic batch under `ownership: 'workflow-lease'`, deposing a still-running
78
+ * owner. Under `ownership: 'lease'` the write still carries the global lease
79
+ * epoch condition (via `commitFencedEngineWrite`'s `workflowId: null` path);
80
+ * under `'none'` it is byte-for-byte unchanged.
33
81
  */
34
- export declare function commitFencedWorkflowStateOperations(internals: EngineInternals, state: WorkflowState, operations: BatchOperation[], options?: WorkflowStateCommitOptions): Promise<void>;
82
+ export declare function commitExternalTerminalWorkflowStateOperations(internals: EngineInternals, state: WorkflowState, operations: BatchOperation[], options?: WorkflowStateCommitOptions): Promise<void>;
35
83
  /** Load and decode persisted schedule state by schedule ID. */
36
84
  export declare function loadScheduleState(internals: EngineInternals, scheduleId: string): Promise<ScheduleState | null>;
37
85
  /** Load a schedule state. */
@@ -14,6 +14,9 @@ import { resolveEffectiveScheduleFireAt } from "./schedule-jitter.js";
14
14
  import { createScheduleTimerId, decodeWorkflowStartHeaders } from "./state-utilities.js";
15
15
  import { decodeWorkflowState } from "./validation.js";
16
16
  import { decodeScheduleState } from "./validation/schedule.js";
17
+ import {
18
+ buildWorkflowClaimExternalTerminalRotationTransition
19
+ } from "./workflow-claim-transitions.js";
17
20
  export async function runSerializedWorkflowStateWrite(internals, workflowId, writeOperation) {
18
21
  const execution = (internals.workflowStateWriteChains.get(workflowId) ?? Promise.resolve()).catch(() => {
19
22
  return;
@@ -51,10 +54,7 @@ export async function loadWorkflowState(internals, workflowId) {
51
54
  return null;
52
55
  return decodeWorkflowState(bytes);
53
56
  }
54
- export async function loadWorkflowResult(internals, workflowId) {
55
- const state = await loadWorkflowState(internals, workflowId);
56
- if (!state)
57
- throw Error(`Workflow "${workflowId}" not found`);
57
+ export function deriveWorkflowResultFromState(state) {
58
58
  if (state.status === "completed")
59
59
  return state.result;
60
60
  if (state.status === "failed") {
@@ -67,9 +67,15 @@ export async function loadWorkflowResult(internals, workflowId) {
67
67
  throw Error("Workflow cancelled");
68
68
  if (state.status === "timed-out") {
69
69
  const elapsed = state.executionDeadline ? state.executionDeadline - getWorkflowExecutionStartedAt(state) : 0;
70
- throw new WorkflowTimeoutError(workflowId, "execution", elapsed, state.terminationReason);
70
+ throw new WorkflowTimeoutError(state.id, "execution", elapsed, state.terminationReason);
71
71
  }
72
- throw Error(`Workflow "${workflowId}" is still ${state.status}`);
72
+ throw Error(`Workflow "${state.id}" is still ${state.status}`);
73
+ }
74
+ export async function loadWorkflowResult(internals, workflowId) {
75
+ const state = await loadWorkflowState(internals, workflowId);
76
+ if (!state)
77
+ throw Error(`Workflow "${workflowId}" not found`);
78
+ return deriveWorkflowResultFromState(state);
73
79
  }
74
80
  function buildWorkflowStateCommit(internals, workflowId, operations, options) {
75
81
  const pendingSideEffects = options.includePendingAtomicSideEffects ? takePendingAtomicWorkflowCommitSideEffects(internals, workflowId) : void 0, operationsWithSideEffects = pendingSideEffects === void 0 ? operations : [...operations, ...pendingSideEffects.operations], conditions = internals.storage.capabilities().conditionalBatch ? pendingSideEffects?.conditions ?? [] : [];
@@ -79,9 +85,21 @@ function buildWorkflowStateCommit(internals, workflowId, operations, options) {
79
85
  hasPendingSideEffects: pendingSideEffects !== void 0
80
86
  };
81
87
  }
82
- export async function commitFencedWorkflowStateOperations(internals, state, operations, options = {}) {
88
+ export async function buildExternalTerminalRotationFragment(internals, workflowId) {
89
+ if (internals.options.ownershipMode !== "workflow-lease")
90
+ return { conditions: [], operations: [] };
91
+ const observedEpochBytes = await internals.storage.get(KEYS.workflowOwnerEpoch(workflowId));
92
+ return buildWorkflowClaimExternalTerminalRotationTransition({ workflowId, observedEpochBytes });
93
+ }
94
+ export async function commitSelfWorkflowStateOperations(internals, state, operations, options = {}) {
83
95
  const commit = buildWorkflowStateCommit(internals, state.id, operations, options);
84
- await commitFencedEngineWrite(internals, commit.operations, commit.conditions, () => Error(`Workflow state commit for workflow "${state.id}" lost its atomic side-effect precondition.`));
96
+ await commitFencedEngineWrite(internals, state.id, commit.operations, commit.conditions, () => Error(`Workflow state commit for workflow "${state.id}" lost its atomic side-effect precondition.`));
97
+ if (commit.hasPendingSideEffects)
98
+ clearPendingAtomicWorkflowCommitSideEffects(internals, state.id);
99
+ }
100
+ export async function commitExternalTerminalWorkflowStateOperations(internals, state, operations, options = {}) {
101
+ const commit = buildWorkflowStateCommit(internals, state.id, operations, options), rotation = await buildExternalTerminalRotationFragment(internals, state.id);
102
+ await commitFencedEngineWrite(internals, null, [...commit.operations, ...rotation.operations], [...commit.conditions, ...rotation.conditions], () => Error(`External terminal commit for workflow "${state.id}" lost a commit precondition (staged side effects, or the ownership epoch rotation under \`workflow-lease\`).`));
85
103
  if (commit.hasPendingSideEffects)
86
104
  clearPendingAtomicWorkflowCommitSideEffects(internals, state.id);
87
105
  }
@@ -132,7 +150,7 @@ export async function writeScheduleState(internals, state, options) {
132
150
  ], includeTimer = options?.includeTimer ?? state.status === "active";
133
151
  operations.push(...buildScheduleTimerReplacementOperations(state, includeTimer, options?.replaceTimerFrom));
134
152
  operations.push(...options?.additionalOperations ?? []);
135
- await commitFencedEngineWrite(internals, operations, [], () => Error(`Schedule state commit for schedule "${state.id}" lost its precondition.`));
153
+ await commitFencedEngineWrite(internals, null, operations, [], () => Error(`Schedule state commit for schedule "${state.id}" lost its precondition.`));
136
154
  }
137
155
  export async function loadWorkflowStartHeaders(internals, workflowId) {
138
156
  const bytes = await internals.storage.get(KEYS.workflowHeaders(workflowId));
@@ -1,6 +1,6 @@
1
- import type { BatchOperation } from '../../../storage/interface.ts';
2
1
  import type { WorkflowState, WorkflowStatus } from '../../types.ts';
3
2
  import type { EngineInternals } from '../internals.ts';
3
+ import type { WorkflowStateCommitCallback } from './state-commit-callbacks.ts';
4
4
  export type TerminationCallbacks = {
5
5
  dispatchEvent: (event: Event) => void;
6
6
  forwardEventToHandle: (workflowId: string, event: Event) => void;
@@ -13,9 +13,10 @@ export type TerminationCallbacks = {
13
13
  handleScheduledWorkflowTerminal: (workflowId: string) => Promise<void>;
14
14
  loadWorkflowState: (workflowId: string) => Promise<WorkflowState | null>;
15
15
  runSerializedWorkflowStateWrite: <Result>(workflowId: string, writeOperation: () => Promise<Result>) => Promise<Result>;
16
- commitWorkflowStateOperations: (state: WorkflowState, operations: BatchOperation[], options?: {
17
- includePendingAtomicSideEffects?: boolean;
18
- }) => Promise<void>;
16
+ /** SELF-transition (complete): fences on this engine's own claim. See ADR 0002. */
17
+ commitSelfWorkflowStateOperations: WorkflowStateCommitCallback;
18
+ /** EXTERNAL terminal transition (suspend): rotates the claim epoch. See ADR 0002. */
19
+ commitExternalTerminalWorkflowStateOperations: WorkflowStateCommitCallback;
19
20
  cleanupReviews: (workflowId: string) => Promise<void>;
20
21
  };
21
22
  export declare const TERMINAL_WORKFLOW_STATUSES: ReadonlySet<WorkflowStatus>;
@@ -64,7 +64,7 @@ export async function terminateWorkflow(internals, workflowId, status, callbacks
64
64
  status,
65
65
  ...reason !== void 0 ? { terminationReason: reason } : {},
66
66
  ...terminalCleanupToken !== void 0 ? { terminalCleanupToken } : {}
67
- }, {
67
+ }, "external-terminal", {
68
68
  allowedStatuses: FORCIBLY_TERMINABLE_STATUSES,
69
69
  buildAdditionalOperations: (_previousState, updatedAt) => {
70
70
  finalizePendingTimelineEntry(internals, workflowId, status, terminationMessage, updatedAt);
@@ -78,6 +78,7 @@ export async function terminateWorkflow(internals, workflowId, status, callbacks
78
78
  });
79
79
  if (!terminationResult)
80
80
  return;
81
+ const claimEpoch = captureCurrentClaimEpoch(internals, workflowId);
81
82
  await runCancellationHandlersForStatus(internals, workflowId, status, callbacks);
82
83
  await releaseWorkflowConcurrencySlot(internals, workflowId);
83
84
  const { previousState, updatedAt } = terminationResult, elapsed = updatedAt - getWorkflowExecutionStartedAt(previousState);
@@ -87,6 +88,7 @@ export async function terminateWorkflow(internals, workflowId, status, callbacks
87
88
  if (previousState.status === "pending")
88
89
  callbacks.swallowPromiseRejection(internals.scheduler.cancel(`delayed-start:${workflowId}`, workflowId));
89
90
  const resolver = internals.resultResolvers.get(workflowId), terminalError = buildTerminalError(workflowId, status, elapsed, reason);
91
+ await releaseWorkflowClaimAfterTerminalSettlement(internals, workflowId, claimEpoch);
90
92
  try {
91
93
  await cleanupTerminalWorkflowSynchronously(internals, workflowId, !0, callbacks);
92
94
  const event = buildTerminalEvent(workflowId, status, elapsed, reason);
@@ -165,6 +167,19 @@ function notifyCompletionWaiters(internals, workflowId, result, duration, callba
165
167
  internals.resultResolvers.delete(workflowId);
166
168
  }
167
169
  }
170
+ function captureCurrentClaimEpoch(internals, workflowId) {
171
+ return internals.workflowClaimRegistry?.currentEpoch(workflowId) ?? null;
172
+ }
173
+ async function releaseWorkflowClaimAfterTerminalSettlement(internals, workflowId, capturedEpoch) {
174
+ const registry = internals.workflowClaimRegistry;
175
+ if (registry === null || capturedEpoch === null)
176
+ return;
177
+ if (registry.currentEpoch(workflowId) !== capturedEpoch)
178
+ return;
179
+ try {
180
+ await registry.release(workflowId);
181
+ } catch {}
182
+ }
168
183
  export async function completeWorkflow(internals, workflowId, result, callbacks) {
169
184
  const completionMetadata = await callbacks.runSerializedWorkflowStateWrite(workflowId, async () => {
170
185
  const state = await callbacks.loadWorkflowState(workflowId);
@@ -180,14 +195,16 @@ export async function completeWorkflow(internals, workflowId, result, callbacks)
180
195
  appendSearchAttributeOperations(completionOperations, workflowId, currentAttributes);
181
196
  if (terminalCleanupToken !== void 0)
182
197
  completionOperations.push(...buildTerminalCleanupTimerOperations(internals, workflowId, !1, now, terminalCleanupToken));
183
- await callbacks.commitWorkflowStateOperations(state, completionOperations, {
198
+ await callbacks.commitSelfWorkflowStateOperations(state, completionOperations, {
184
199
  includePendingAtomicSideEffects: !0
185
200
  });
186
- return { duration };
201
+ const claimEpoch = captureCurrentClaimEpoch(internals, workflowId);
202
+ return { duration, claimEpoch };
187
203
  });
188
204
  if (!completionMetadata)
189
205
  return;
190
206
  await releaseWorkflowConcurrencySlot(internals, workflowId);
207
+ await releaseWorkflowClaimAfterTerminalSettlement(internals, workflowId, completionMetadata.claimEpoch);
191
208
  notifyCompletionWaiters(internals, workflowId, result, completionMetadata.duration, callbacks);
192
209
  }
193
210
  export async function failWorkflow(internals, workflowId, error, callbacks, failureCategory = "system") {
@@ -202,7 +219,7 @@ export async function failWorkflow(internals, workflowId, error, callbacks, fail
202
219
  stateUpdate.errorStack = error.stack;
203
220
  if (terminalCleanupToken !== void 0)
204
221
  stateUpdate.terminalCleanupToken = terminalCleanupToken;
205
- if (!await updateWorkflowState(internals, workflowId, stateUpdate, {
222
+ if (!await updateWorkflowState(internals, workflowId, stateUpdate, "self", {
206
223
  allowedStatuses: FORCIBLY_TERMINABLE_STATUSES,
207
224
  buildAdditionalOperations: (_previousState, updatedAt) => {
208
225
  finalizePendingTimelineEntry(internals, workflowId, "failed", error.message, updatedAt);
@@ -214,11 +231,13 @@ export async function failWorkflow(internals, workflowId, error, callbacks, fail
214
231
  }
215
232
  }))
216
233
  return;
234
+ const claimEpoch = captureCurrentClaimEpoch(internals, workflowId);
217
235
  await releaseWorkflowConcurrencySlot(internals, workflowId);
218
236
  await cleanupAttributeIndex(internals, workflowId, attributes);
219
237
  callbacks.swallowPromiseRejection(internals.scheduler.cancel(`deadline:${workflowId}`, workflowId));
220
238
  await writeRetainedTerminalSearchAttributes(internals, workflowId, retainedAttributes);
221
239
  const resolver = internals.resultResolvers.get(workflowId);
240
+ await releaseWorkflowClaimAfterTerminalSettlement(internals, workflowId, claimEpoch);
222
241
  try {
223
242
  await cleanupTerminalWorkflowSynchronously(internals, workflowId, !1, callbacks);
224
243
  const event = new WorkflowFailedEvent(workflowId, error);
@@ -45,22 +45,22 @@ export function encodeRunningClaim(attempts, token, claimedAt) {
45
45
  export async function rearmTeardownTimer(internals, workflowId, token, delayMs) {
46
46
  const fireAt = internals.options.getNow() + delayMs;
47
47
  try {
48
- await commitFencedEngineWrite(internals, teardownTimerOperations(token, workflowId, fireAt), [], () => Error("teardown self-heal re-arm lost the lease fence"));
48
+ await commitFencedEngineWrite(internals, workflowId, teardownTimerOperations(token, workflowId, fireAt), [], () => Error("teardown self-heal re-arm lost the lease fence"));
49
49
  } catch {}
50
50
  }
51
51
  export async function clearTeardownMarker(internals, workflowId, expectedBytes) {
52
52
  try {
53
- return await commitFencedEngineWriteAllowingPreconditionFailure(internals, [{ type: "delete", key: KEYS.teardownOwed(workflowId) }], [{ key: KEYS.teardownOwed(workflowId), expectedValue: expectedBytes }]);
53
+ return await commitFencedEngineWriteAllowingPreconditionFailure(internals, workflowId, [{ type: "delete", key: KEYS.teardownOwed(workflowId) }], [{ key: KEYS.teardownOwed(workflowId), expectedValue: expectedBytes }]);
54
54
  } catch {
55
55
  return !1;
56
56
  }
57
57
  }
58
58
  export async function claimTeardownMarker(internals, workflowId, expectedBytes, attempts, token) {
59
59
  const runningBytes = encodeRunningClaim(attempts, token, internals.options.getNow());
60
- return await commitFencedEngineWriteAllowingPreconditionFailure(internals, [{ type: "put", key: KEYS.teardownOwed(workflowId), value: runningBytes }], [{ key: KEYS.teardownOwed(workflowId), expectedValue: expectedBytes }]) ? runningBytes : null;
60
+ return await commitFencedEngineWriteAllowingPreconditionFailure(internals, workflowId, [{ type: "put", key: KEYS.teardownOwed(workflowId), value: runningBytes }], [{ key: KEYS.teardownOwed(workflowId), expectedValue: expectedBytes }]) ? runningBytes : null;
61
61
  }
62
62
  export async function settleOnRunningClaim(internals, workflowId, runningBytes, operations) {
63
- return commitFencedEngineWriteAllowingPreconditionFailure(internals, operations, [
63
+ return commitFencedEngineWriteAllowingPreconditionFailure(internals, workflowId, operations, [
64
64
  { key: KEYS.teardownOwed(workflowId), expectedValue: runningBytes }
65
65
  ]);
66
66
  }
@@ -0,0 +1,15 @@
1
+ import type { BatchOperation } from '../../../storage/interface.ts';
2
+ import type { WorkflowState } from '../../types.ts';
3
+ /**
4
+ * Shared function shape for the two workflow-state commit callbacks
5
+ * {@link TerminationCallbacks} exposes, split by ADR 0002's self/
6
+ * external-terminal transition classification (see
7
+ * `documentation/contributing/architecture-decisions/0002-multiengine-per-workflow-ownership.md`).
8
+ * A self-transition (complete, fail) fences on this engine's own claim; an
9
+ * external terminal transition (cancel, timeout, suspend) rotates the claim
10
+ * epoch instead, so any engine may commit it. Kept as one shared type so the
11
+ * two `TerminationCallbacks` fields cannot drift in shape.
12
+ */
13
+ export type WorkflowStateCommitCallback = (state: WorkflowState, operations: BatchOperation[], options?: {
14
+ includePendingAtomicSideEffects?: boolean;
15
+ }) => Promise<void>;
@@ -18,7 +18,7 @@ export async function suspendWorkflow(internals, workflowId, callbacks) {
18
18
  internals.parkedInlineWorkflows.delete(workflowId);
19
19
  evictSuspendedWorkflowWaiters(internals, workflowId, callbacks);
20
20
  const updatedAt = internals.options.getNow(), updatedState = { ...state, status: "suspended", updatedAt };
21
- await callbacks.commitWorkflowStateOperations(state, [
21
+ await callbacks.commitExternalTerminalWorkflowStateOperations(state, [
22
22
  { type: "put", key: KEYS.workflow(workflowId), value: encode(updatedState) },
23
23
  ...buildWorkflowVisibilityIndexTransition(workflowId, state, updatedState).batchOps,
24
24
  ...buildDeadlineTimerDeleteOperations(workflowId, state.executionDeadline)
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Pre-acceptance validation for workflow updates: running a registered
3
+ * `updateValidators` entry and normalizing a Standard Schema v1 failure result
4
+ * into the issue list `UpdateValidationError` carries.
5
+ *
6
+ * Lives apart from `updates.ts` because it is shared — `pending-updates.ts`
7
+ * normalizes validator results through the same helper — and because it is
8
+ * pure payload validation with no coupling to the in-memory waiter maps or the
9
+ * durable coordinated-update protocol that make up the rest of update delivery.
10
+ *
11
+ * @module core/engine/update-validation
12
+ */
13
+ import type { EngineInternals } from './internals.ts';
14
+ /**
15
+ * Run the pre-acceptance validator for an update, if one is registered.
16
+ * Throws `UpdateValidationError` if the validator rejects (by throwing or by
17
+ * returning a Standard Schema `{ issues: [...] }` failure result).
18
+ */
19
+ export declare function runUpdateValidator(internals: EngineInternals, workflowId: string, name: string, payload: unknown): Promise<void>;
20
+ /**
21
+ * Extract issues from a Standard Schema v1 failure result, or null if absent.
22
+ * No string-`message` entries yields `[]`; callers reject only on a non-empty
23
+ * array, so `null` and `[]` both mean acceptance. Preserves `path` (RFC 6901).
24
+ */
25
+ export declare function extractStandardSchemaIssues(result: unknown): Array<{
26
+ message: string;
27
+ path?: string;
28
+ }> | null;
@@ -0,0 +1,37 @@
1
+ import { UpdateValidationError } from "../updates.js";
2
+ export async function runUpdateValidator(internals, workflowId, name, payload) {
3
+ const validator = internals.inlineStrategy?.getContext(workflowId)?.updateValidators.get(name);
4
+ if (validator === void 0)
5
+ return;
6
+ let result;
7
+ try {
8
+ result = await validator(payload);
9
+ } catch (error) {
10
+ const message = error instanceof Error ? error.message : String(error);
11
+ throw new UpdateValidationError(name, [{ message }]);
12
+ }
13
+ const issues = extractStandardSchemaIssues(result);
14
+ if (issues !== null && issues.length > 0)
15
+ throw new UpdateValidationError(name, issues);
16
+ }
17
+ export function extractStandardSchemaIssues(result) {
18
+ if (result === null || typeof result !== "object" || !("issues" in result))
19
+ return null;
20
+ const { issues } = result;
21
+ if (!Array.isArray(issues))
22
+ return null;
23
+ return issues.flatMap((issue) => {
24
+ if (issue === null || typeof issue !== "object")
25
+ return [];
26
+ const obj = issue;
27
+ if (typeof obj.message !== "string")
28
+ return [];
29
+ const entry = { message: obj.message };
30
+ if (Array.isArray(obj.path) && obj.path.length > 0)
31
+ entry.path = obj.path.reduce((p, seg) => {
32
+ const k = seg !== null && typeof seg === "object" && "key" in seg ? String(seg.key) : String(seg);
33
+ return p + "/" + k.replace(/~/g, "~0").replace(/\//g, "~1");
34
+ }, "");
35
+ return [entry];
36
+ });
37
+ }
@@ -1,6 +1,6 @@
1
1
  import type { ContextOperationRequest } from '../context.ts';
2
2
  import type { CoordinatedUpdateResult } from '../types.ts';
3
- import { type UpdateRequest, type UpdateResponse } from '../updates.ts';
3
+ import type { UpdateRequest, UpdateResponse } from '../updates.ts';
4
4
  import type { EngineInternals } from './internals.ts';
5
5
  export type UpdateCallbacks = {
6
6
  dispatchEvent: (event: Event) => boolean;
@@ -22,6 +22,22 @@ export type UpdateCallbacks = {
22
22
  export declare function update(internals: EngineInternals, workflowId: string, name: string, payload: unknown, options: {
23
23
  timeout?: number;
24
24
  } | undefined, callbacks: UpdateCallbacks): Promise<unknown>;
25
+ /**
26
+ * `handled: false` used to conflate two reasons: no live local context at all
27
+ * (`'not-owned-locally'`, possible under `ownership: 'workflow-lease'` when
28
+ * another engine holds the claim) versus a live context with no handler
29
+ * registered for `name` (`'no-handler'`). See the `update()` call site for
30
+ * why neither is routed differently — the reason is exposed so a caller that
31
+ * DOES care (tests, future routing) can tell them apart.
32
+ */
33
+ export type InlineUpdateAttemptResult = {
34
+ handled: true;
35
+ value: unknown;
36
+ } | {
37
+ handled: false;
38
+ reason: 'not-owned-locally' | 'no-handler';
39
+ };
40
+ export declare function tryInlineUpdateHandler(internals: EngineInternals, workflowId: string, name: string, payload: unknown, callbacks: UpdateCallbacks): Promise<InlineUpdateAttemptResult>;
25
41
  /** Retrieve the result of a coordinated update by its ID. */
26
42
  export declare function getUpdateResult(internals: EngineInternals, updateId: string): Promise<UpdateResponse | null>;
27
43
  /**
@@ -39,18 +55,3 @@ export declare function dispatchPendingUpdateReceived(_internals: EngineInternal
39
55
  export declare function createCoordinatedUpdateResponder(_internals: EngineInternals, workflowId: string, updateName: string, updateRequest: UpdateRequest, callbacks: Pick<UpdateCallbacks, 'persistCoordinatedUpdateResponse'>): (value: unknown) => void;
40
56
  export declare function deliverCoordinatedUpdateToWaiterIfAvailable(internals: EngineInternals, workflowId: string, updateRequest: UpdateRequest, dispatchReceivedEvent: boolean | undefined, callbacks: UpdateCallbacks): Promise<boolean>;
41
57
  export declare function findPendingUpdateByName(internals: EngineInternals, workflowId: string, name: string): Promise<UpdateRequest | undefined>;
42
- /**
43
- * Invoke an update handler, checking that it does not return a generator.
44
- * Centralises the runtime generator guard for both the inline-handler path
45
- * in `update()` and the pending-drain path on resume.
46
- */
47
- export declare function invokeUpdateHandler(_internals: EngineInternals, name: string, handler: (payload: unknown) => unknown, payload: unknown): Promise<unknown>;
48
- /**
49
- * Extract issues from a Standard Schema v1 failure result, or null if absent.
50
- * No string-`message` entries yields `[]`; callers reject only on a non-empty
51
- * array, so `null` and `[]` both mean acceptance. Preserves `path` (RFC 6901).
52
- */
53
- export declare function extractStandardSchemaIssues(result: unknown): Array<{
54
- message: string;
55
- path?: string;
56
- }> | null;