@fabricorg/platform-host 8.1.2 → 8.2.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.
package/dist/index.cjs CHANGED
@@ -96,6 +96,78 @@ function normalizeProvenance(input, options) {
96
96
  };
97
97
  }
98
98
  //#endregion
99
+ //#region src/action-finalization.ts
100
+ /** Strict JSON evidence: reject lossy serialization rather than silently dropping values. */
101
+ function finalizationJson(value) {
102
+ const ancestors = /* @__PURE__ */ new Set();
103
+ function check(item) {
104
+ if (item === null || typeof item === "string" || typeof item === "boolean") return;
105
+ if (typeof item === "number" && Number.isFinite(item)) return;
106
+ if (typeof item !== "object" || !item || ancestors.has(item)) throw new Error("Finalization requires JSON-compatible data.");
107
+ if (!Array.isArray(item) && Object.getPrototypeOf(item) !== Object.prototype && Object.getPrototypeOf(item) !== null) throw new Error("Finalization requires plain JSON objects.");
108
+ if (Object.getOwnPropertySymbols(item).length > 0) throw new Error("Finalization requires JSON string keys.");
109
+ if (Array.isArray(item)) {
110
+ const keys = Object.keys(item);
111
+ if (keys.length !== item.length || keys.some((key, index) => key !== String(index))) throw new Error("Finalization requires dense JSON arrays.");
112
+ }
113
+ ancestors.add(item);
114
+ for (const child of Object.values(item)) check(child);
115
+ ancestors.delete(item);
116
+ }
117
+ check(value);
118
+ return JSON.parse(JSON.stringify(value));
119
+ }
120
+ /** Match the persisted JSON representation before validating finalization execution input. */
121
+ function normalizeFinalizationJson(value) {
122
+ return finalizationJson(JSON.parse(JSON.stringify(value)));
123
+ }
124
+ /** Omit only public optional fields; payloads and patch values retain strict JSON validation. */
125
+ function omitUndefinedOptionalFields(value, keys) {
126
+ if (!value || typeof value !== "object" || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) return value;
127
+ const copy = { ...value };
128
+ for (const key of keys) if (Object.hasOwn(copy, key) && copy[key] === void 0) delete copy[key];
129
+ return copy;
130
+ }
131
+ function validateFinalization(value, action, privateFields, contracts) {
132
+ const normalized = omitUndefinedOptionalFields(value, ["events", "resultPatch"]);
133
+ if (normalized !== value && normalized && typeof normalized === "object" && !Array.isArray(normalized)) {
134
+ const record = normalized;
135
+ if (Array.isArray(record.events)) {
136
+ const events = Object.assign(record.events.slice(), record.events);
137
+ for (let index = 0; index < events.length; index++) if (Object.hasOwn(events, index)) events[index] = omitUndefinedOptionalFields(events[index], ["eventSchemaVersion"]);
138
+ record.events = events;
139
+ }
140
+ }
141
+ const result = finalizationJson(normalized);
142
+ if (!result || typeof result !== "object" || Array.isArray(result) || Object.keys(result).some((key) => key !== "events" && key !== "resultPatch")) throw new Error("Finalization must return only events and resultPatch.");
143
+ if (result.resultPatch !== void 0) {
144
+ if (!result.resultPatch || typeof result.resultPatch !== "object" || Array.isArray(result.resultPatch)) throw new Error("Finalization resultPatch must be an object.");
145
+ if (Object.keys(result.resultPatch).some((key) => privateFields.includes(key))) throw new Error("Finalization resultPatch contains a private result key.");
146
+ }
147
+ if (result.events !== void 0) {
148
+ if (!Array.isArray(result.events)) throw new Error("Finalization events must be an array.");
149
+ for (const event of result.events) {
150
+ if (!event || typeof event !== "object" || Array.isArray(event) || Object.keys(event).some((key) => ![
151
+ "eventType",
152
+ "subjectType",
153
+ "subjectId",
154
+ "payload",
155
+ "eventSchemaVersion"
156
+ ].includes(key))) throw new Error("Invalid finalization event fields.");
157
+ for (const key of [
158
+ "eventType",
159
+ "subjectType",
160
+ "subjectId"
161
+ ]) if (typeof event[key] !== "string" || !event[key].trim()) throw new Error("Finalization event identifiers must be non-empty.");
162
+ if (!Object.hasOwn(event, "payload")) throw new Error("Finalization event requires a JSON payload.");
163
+ if (event.eventSchemaVersion !== void 0 && (!Number.isInteger(event.eventSchemaVersion) || event.eventSchemaVersion <= 0)) throw new Error("Finalization eventSchemaVersion must be a positive integer.");
164
+ if (!action.emitsEvents.includes(event.eventType)) throw new Error("Finalization emitted undeclared event type.");
165
+ if (contracts && (!contracts.hasEventType(event.eventType) || !contracts.hasSubjectType(event.subjectType))) throw new Error("Finalization emitted an unregistered event or subject type.");
166
+ }
167
+ }
168
+ return result;
169
+ }
170
+ //#endregion
99
171
  //#region src/observability.ts
100
172
  /** Version of the vendor-neutral health/readiness contract. */
101
173
  const PLATFORM_HOST_HEALTH_CONTRACT_VERSION = 1;
@@ -1463,6 +1535,7 @@ function createGovernedActionHost(options) {
1463
1535
  await attemptEvidenceWrites(evidenceWrites);
1464
1536
  }
1465
1537
  async function failExecution(record, status, error) {
1538
+ if (record.finalizationCheckpoint && hasDurableProviderEffect) return settleAppliedEffect(error);
1466
1539
  await persistExecutionInvocation(record, {
1467
1540
  status,
1468
1541
  error
@@ -1755,14 +1828,34 @@ function createGovernedActionHost(options) {
1755
1828
  };
1756
1829
  }
1757
1830
  const action = actionResolver(invocation.actionId);
1758
- if (action && (resumingRunningInvocation || invocation.attemptCount > 1)) try {
1759
- for (const [stepIndex] of (action.adapterSteps ?? []).entries()) {
1831
+ let hasNonSucceededAdapterRow = false;
1832
+ if ((action || invocation.finalizationCheckpoint) && (resumingRunningInvocation || invocation.attemptCount > 1)) try {
1833
+ let runningCheckpoint;
1834
+ for (const stepIndex of Array.from({ length: Math.max((action?.adapterSteps ?? []).length, invocation.finalizationCheckpoint?.steps.length ?? 0) }, (_, index) => index)) {
1760
1835
  const record = await options.store.getAdapterInvocation(lifecycleId("adp", actionInvocationId, String(stepIndex)), adapterInvocationScope);
1761
- if (record) adapterStatuses.set(record.id, record.status);
1836
+ if (record) {
1837
+ adapterStatuses.set(record.id, record.status);
1838
+ hasNonSucceededAdapterRow ||= record.status !== "succeeded";
1839
+ }
1762
1840
  if (record?.status === "succeeded") hasDurableProviderEffect = true;
1841
+ if (invocation.finalizationCheckpoint && record?.status === "running") runningCheckpoint ??= {
1842
+ stepIndex,
1843
+ record
1844
+ };
1763
1845
  if (record?.status !== "ambiguous") continue;
1764
1846
  return await settleUnknownOutcome(stepIndex, recordedAdapterReconciliation(record), record.reconciliation?.adapterEventSubject);
1765
1847
  }
1848
+ if (runningCheckpoint) {
1849
+ const { stepIndex, record } = runningCheckpoint;
1850
+ return await settleUnknownOutcome(stepIndex, {
1851
+ kind: "adapter_outcome_ambiguous",
1852
+ adapterInvocationId: record.id,
1853
+ adapterType: record.adapterType,
1854
+ operation: record.operation,
1855
+ vendor: record.vendor,
1856
+ message: "Recorded adapter outcome is unknown."
1857
+ });
1858
+ }
1766
1859
  } catch (error) {
1767
1860
  if (error instanceof RecoverableFinalizationError) throw error;
1768
1861
  throw new RecoverableFinalizationError(caughtError({
@@ -1772,6 +1865,30 @@ function createGovernedActionHost(options) {
1772
1865
  }
1773
1866
  if (invocation.status === "running" && invocation.pendingCompletion && !invocation.leaseOwner) return withConsistency(actionResult(invocation), invocation.actionId);
1774
1867
  if (!action) return hostOperation(() => failExecution(invocation, "failed", `Unknown action: ${invocation.actionId}`));
1868
+ if (invocation.finalizationCheckpoint && !action.finalize) return interruptedNonIdempotent("Action no longer declares its checkpointed finalizer.");
1869
+ let hasFencedAdapterWrites = false;
1870
+ try {
1871
+ (0, _fabricorg_platform.assertAdapterStepDeadlines)(action);
1872
+ (0, _fabricorg_platform.assertActionFinalization)(action);
1873
+ if (action.finalize) {
1874
+ const store = asAtomicMutationStore(options.store);
1875
+ if (!store) throw new Error("Action finalization requires atomic mutation transactions.");
1876
+ await store.transactionWithEvents(async (transaction) => {
1877
+ if (typeof transaction.getActionInvocationForUpdate !== "function" || typeof transaction.updateLeasedActionInvocation !== "function" || options.outbox && typeof transaction.appendEventWithOutbox !== "function") throw new Error("Action finalization requires atomic invocation locking and transaction-scoped lease fencing.");
1878
+ hasFencedAdapterWrites = typeof options.store.writeAdapterInvocation === "function" && typeof transaction.writeAdapterInvocation === "function";
1879
+ });
1880
+ }
1881
+ } catch (error) {
1882
+ return interruptedNonIdempotent(caughtError({
1883
+ actionId: action.actionId,
1884
+ source: "execution"
1885
+ }, error));
1886
+ }
1887
+ const replayCheckpoint = action.finalize && (resumingRunningInvocation || invocation.attemptCount > 1) ? invocation.finalizationCheckpoint : void 0;
1888
+ if (replayCheckpoint) {
1889
+ if (replayCheckpoint.protocol !== "atomic-after-adapters-v1" || replayCheckpoint.actionVersion !== action.version || replayCheckpoint.steps.length !== (action.adapterSteps ?? []).length || replayCheckpoint.steps.some((step, index) => step.adapterType !== action.adapterSteps[index]?.adapterType || step.operation !== action.adapterSteps[index]?.operation) || invocation.pendingCompletion) return interruptedNonIdempotent("Finalization checkpoint protocol, action version, step identity or handoff is incompatible with replay.");
1890
+ }
1891
+ if (action.finalize && !replayCheckpoint && (resumingRunningInvocation || invocation.attemptCount > 1) && hasDurableProviderEffect) return interruptedNonIdempotent("Finalizing action recovery requires a preparation checkpoint; reconciliation is required.");
1775
1892
  if (action.admitSubmission !== void 0 && !isAdmissionTime(invocation.admittedAt)) return hostOperation(() => failExecution(invocation, "failed", `Invocation of ${action.actionId} was not admitted at submission; an action that declares admitSubmission runs only on invocations its check admitted.`));
1776
1893
  if (action.kind === "saga") return hostOperation(() => failExecution(invocation, "failed", `Saga action ${invocation.actionId} requires a durable PlatformActionDispatcher and cannot execute as an atomic Host action.`));
1777
1894
  async function interruptedNonIdempotent(message) {
@@ -1801,8 +1918,9 @@ function createGovernedActionHost(options) {
1801
1918
  }
1802
1919
  return hostOperation(() => failExecution(invocation, "failed", message));
1803
1920
  }
1804
- if (resumingRunningInvocation && invocation.attemptCount === 0 && !invocation.leaseOwner && !action.idempotent) return interruptedNonIdempotent(`Interrupted inline action ${invocation.actionId} is not declared idempotent; manual reconciliation is required.`);
1805
- if (invocation.attemptCount > 1 && !action.idempotent) return interruptedNonIdempotent(`Interrupted action ${invocation.actionId} is not declared idempotent; manual reconciliation is required.`);
1921
+ const canReplayNonIdempotent = !!replayCheckpoint && !hasNonSucceededAdapterRow && (!executionLease || hasFencedAdapterWrites);
1922
+ if (resumingRunningInvocation && invocation.attemptCount === 0 && !invocation.leaseOwner && !action.idempotent && !canReplayNonIdempotent) return interruptedNonIdempotent(`Interrupted inline action ${invocation.actionId} is not declared idempotent; manual reconciliation is required.`);
1923
+ if (invocation.attemptCount > 1 && !action.idempotent && !canReplayNonIdempotent && invocation.deferredBeforeEffectAttempt !== invocation.attemptCount - 1) return interruptedNonIdempotent(`Interrupted action ${invocation.actionId} is not declared idempotent; manual reconciliation is required.`);
1806
1924
  if (invocation.status !== "running") await hostOperation(() => options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, { status: "running" }));
1807
1925
  emitPlatformHostTelemetry(options.telemetry, {
1808
1926
  kind: "event",
@@ -2003,144 +2121,209 @@ function createGovernedActionHost(options) {
2003
2121
  }))));
2004
2122
  }
2005
2123
  }
2124
+ const preparedInputs = /* @__PURE__ */ new Map();
2125
+ let preparation = replayCheckpoint;
2006
2126
  let data;
2127
+ let finalizationInput;
2007
2128
  let domainEvents = [];
2008
2129
  let pendingTransitionEvent;
2009
2130
  try {
2010
- const runHandler = async (db, transaction) => {
2011
- if (options.outbox && !transaction?.appendEventWithOutbox) throw new HostError("Outbox egress requires transactionWithEvents to provide appendEventWithOutbox before handler execution.");
2012
- const binding = action.stateMachine;
2013
- let replayedTransition;
2014
- let pendingStateTransition;
2015
- let validatedTransition;
2016
- if (binding) {
2017
- const machine = stateMachineResolver(binding.entityType);
2018
- const entityId = binding.getEntityId(parsed.data);
2019
- const getEntityState = transaction?.getEntityState ? transaction.getEntityState.bind(transaction) : options.store.getEntityState.bind(options.store);
2020
- const currentState = entityId ? await hostOperation(() => getEntityState(tenantId, spaceId, binding.entityType, entityId)) ?? initialState(machine) : initialState(machine);
2021
- const targetState = typeof binding.targetState === "function" ? binding.targetState(parsed.data) : binding.targetState;
2022
- if (targetState !== "") {
2023
- const transition = await (0, _fabricorg_platform.evaluateStateMachineTransition)(machine, binding.entityType, currentState, targetState, action.actionId, {
2024
- entity: {
2025
- ...entityId ? { id: entityId } : {},
2026
- state: currentState
2027
- },
2028
- actionInvocationId,
2029
- actorId: invocation.actorId,
2030
- parameters: parsed.data,
2031
- db
2032
- });
2033
- validatedTransition = {
2034
- entityId,
2035
- currentState,
2036
- targetState,
2037
- valid: transition.valid
2038
- };
2039
- let replayingAppliedTransition = false;
2040
- if (action.idempotent && currentState === targetState && !transition.valid && entityId) {
2041
- let source;
2042
- if (transaction?.getEntityStateSource) source = await hostOperation(() => transaction.getEntityStateSource(tenantId, spaceId, binding.entityType, entityId));
2043
- else if (options.store.getEntityStateSource) source = await hostOperation(() => options.store.getEntityStateSource(tenantId, spaceId, binding.entityType, entityId));
2044
- else {
2045
- const eventStore = options.store;
2046
- const listEvents = transaction?.listEvents?.bind(transaction) ?? eventStore.listEvents?.bind(eventStore);
2047
- const events = listEvents ? await hostOperation(() => listEvents(tenantId, spaceId)) : [];
2048
- let latest;
2049
- for (const event of events) if (event.tenantId === tenantId && event.spaceId === spaceId && event.subjectType === binding.entityType && event.subjectId === entityId && typeof event.payload?.toState === "string" && (!latest || event.sequence > latest.sequence)) latest = event;
2050
- if (latest && typeof latest.actionInvocationId === "string") source = {
2051
- state: latest.payload.toState,
2052
- actionInvocationId: latest.actionInvocationId
2131
+ if (replayCheckpoint) {
2132
+ data = structuredClone(replayCheckpoint.result);
2133
+ finalizationInput = structuredClone({
2134
+ params: replayCheckpoint.params,
2135
+ handlerData: replayCheckpoint.handlerData
2136
+ });
2137
+ domainEvents = structuredClone(replayCheckpoint.handlerEvents);
2138
+ pendingTransitionEvent = structuredClone(replayCheckpoint.stateTransition);
2139
+ } else {
2140
+ const runHandler = async (db, transaction) => {
2141
+ if (options.outbox && !transaction?.appendEventWithOutbox) throw new HostError("Outbox egress requires transactionWithEvents to provide appendEventWithOutbox before handler execution.");
2142
+ if (action.finalize) {
2143
+ const current = await transaction.getActionInvocationForUpdate(actionInvocationId, tenantId, spaceId);
2144
+ if (!current || current.status !== "running" || current.finalizationCheckpoint || (executionLease ? current.leaseOwner !== executionLease.leaseOwner || current.leaseToken !== executionLease.leaseToken : !!current.leaseOwner)) throw new RecoverableFinalizationError(`Invocation preparation ownership changed: ${actionInvocationId}`);
2145
+ }
2146
+ const binding = action.stateMachine;
2147
+ let replayedTransition;
2148
+ let pendingStateTransition;
2149
+ let validatedTransition;
2150
+ if (binding) {
2151
+ const machine = stateMachineResolver(binding.entityType);
2152
+ const entityId = binding.getEntityId(parsed.data);
2153
+ const getEntityState = transaction?.getEntityState ? transaction.getEntityState.bind(transaction) : options.store.getEntityState.bind(options.store);
2154
+ const currentState = entityId ? await hostOperation(() => getEntityState(tenantId, spaceId, binding.entityType, entityId)) ?? initialState(machine) : initialState(machine);
2155
+ const targetState = typeof binding.targetState === "function" ? binding.targetState(parsed.data) : binding.targetState;
2156
+ if (targetState !== "") {
2157
+ const transition = await (0, _fabricorg_platform.evaluateStateMachineTransition)(machine, binding.entityType, currentState, targetState, action.actionId, {
2158
+ entity: {
2159
+ ...entityId ? { id: entityId } : {},
2160
+ state: currentState
2161
+ },
2162
+ actionInvocationId,
2163
+ actorId: invocation.actorId,
2164
+ parameters: parsed.data,
2165
+ db
2166
+ });
2167
+ validatedTransition = {
2168
+ entityId,
2169
+ currentState,
2170
+ targetState,
2171
+ valid: transition.valid
2172
+ };
2173
+ let replayingAppliedTransition = false;
2174
+ if (action.idempotent && currentState === targetState && !transition.valid && entityId) {
2175
+ let source;
2176
+ if (transaction?.getEntityStateSource) source = await hostOperation(() => transaction.getEntityStateSource(tenantId, spaceId, binding.entityType, entityId));
2177
+ else if (options.store.getEntityStateSource) source = await hostOperation(() => options.store.getEntityStateSource(tenantId, spaceId, binding.entityType, entityId));
2178
+ else {
2179
+ const eventStore = options.store;
2180
+ const listEvents = transaction?.listEvents?.bind(transaction) ?? eventStore.listEvents?.bind(eventStore);
2181
+ const events = listEvents ? await hostOperation(() => listEvents(tenantId, spaceId)) : [];
2182
+ let latest;
2183
+ for (const event of events) if (event.tenantId === tenantId && event.spaceId === spaceId && event.subjectType === binding.entityType && event.subjectId === entityId && typeof event.payload?.toState === "string" && (!latest || event.sequence > latest.sequence)) latest = event;
2184
+ if (latest && typeof latest.actionInvocationId === "string") source = {
2185
+ state: latest.payload.toState,
2186
+ actionInvocationId: latest.actionInvocationId
2187
+ };
2188
+ }
2189
+ replayingAppliedTransition = source?.actionInvocationId === actionInvocationId && source.state === currentState;
2190
+ }
2191
+ const pending = invocation.pendingStateTransition;
2192
+ if (replayingAppliedTransition && pending && pending.entityType === binding.entityType && pending.entityId === entityId && pending.toState === currentState) replayedTransition = pending;
2193
+ if (action.idempotent && transition.valid && entityId && action.eventPhase === "after_adapters") {
2194
+ pendingStateTransition = {
2195
+ entityType: binding.entityType,
2196
+ entityId,
2197
+ fromState: currentState,
2198
+ toState: targetState
2053
2199
  };
2200
+ if (!transaction) await hostOperation(() => options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, { pendingStateTransition }));
2054
2201
  }
2055
- replayingAppliedTransition = source?.actionInvocationId === actionInvocationId && source.state === currentState;
2202
+ if (!transition.valid && !replayingAppliedTransition) throw transition.error != null ? new Error(transition.error) : new HostError("Invalid state transition");
2056
2203
  }
2057
- const pending = invocation.pendingStateTransition;
2058
- if (replayingAppliedTransition && pending && pending.entityType === binding.entityType && pending.entityId === entityId && pending.toState === currentState) replayedTransition = pending;
2059
- if (action.idempotent && transition.valid && entityId && action.eventPhase === "after_adapters") {
2060
- pendingStateTransition = {
2204
+ }
2205
+ const handlerResult = action.handler ? await action.handler({
2206
+ actionInvocationId,
2207
+ tenantId,
2208
+ spaceId,
2209
+ actorId: invocation.actorId,
2210
+ actorType: invocation.actorType,
2211
+ correlationId: invocation.correlationId,
2212
+ ...invocation.causationId ? { causationId: invocation.causationId } : {},
2213
+ ...invocation.provenance ? { provenance: invocation.provenance } : {},
2214
+ ...isAdmissionTime(invocation.admittedAt) ? { admittedAt: invocation.admittedAt } : {},
2215
+ db,
2216
+ services: options.services,
2217
+ ...action.finalize ? { finalizationProtocol: "atomic-after-adapters-v1" } : {}
2218
+ }, parsed.data) : {
2219
+ success: true,
2220
+ data: {}
2221
+ };
2222
+ if (!handlerResult.success) throw handlerResult.error != null ? new Error(handlerResult.error) : new HostError("Action handler failed");
2223
+ const handlerData = handlerResult.data ?? {};
2224
+ if (action.resultSchema && !action.finalize) {
2225
+ const publicResult = withoutPrivateHostFields(handlerData, eventResultFields);
2226
+ const parsedResult = action.resultSchema.safeParse(publicResult);
2227
+ if (!parsedResult.success) throw new HostError(`Action result validation failed at ${parsedResult.error.issues.map((issue) => issue.path.map(String).join(".") || "result").join(", ")}`);
2228
+ }
2229
+ const preparedFinalizationInput = action.finalize ? {
2230
+ params: normalizeFinalizationJson(parsed.data),
2231
+ handlerData: normalizeFinalizationJson(withoutPrivateHostFields(handlerData, eventResultFields))
2232
+ } : void 0;
2233
+ const extractedEvents = extractEvents(handlerData);
2234
+ const events = action.finalize ? normalizeFinalizationJson(extractedEvents) : extractedEvents;
2235
+ const undeclaredEvent = events.find((event) => !action.emitsEvents.includes(event.eventType));
2236
+ if (undeclaredEvent) throw new HostError(`${action.actionId} emitted undeclared event type ${undeclaredEvent.eventType}`);
2237
+ if (action.eventPhase !== "after_adapters") for (const [index, event] of events.entries()) await hostOperation(() => appendEvent(invocation, event, `domain:${index}`, action.version, transaction));
2238
+ let transitionEvent;
2239
+ if (binding && validatedTransition?.entityId && (validatedTransition.valid || replayedTransition)) {
2240
+ const { entityId, currentState, targetState } = validatedTransition;
2241
+ if (!events.some((event) => event.eventType === "StateTransitioned" && event.subjectType === binding.entityType && event.subjectId === entityId)) transitionEvent = {
2242
+ eventType: "StateTransitioned",
2243
+ subjectType: binding.entityType,
2244
+ subjectId: entityId,
2245
+ payload: {
2061
2246
  entityType: binding.entityType,
2062
2247
  entityId,
2063
- fromState: currentState,
2064
- toState: targetState
2065
- };
2066
- if (!transaction) await hostOperation(() => options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, { pendingStateTransition }));
2067
- }
2068
- if (!transition.valid && !replayingAppliedTransition) throw transition.error != null ? new Error(transition.error) : new HostError("Invalid state transition");
2248
+ fromState: replayedTransition?.fromState ?? currentState,
2249
+ toState: targetState,
2250
+ causedByAction: action.actionId
2251
+ }
2252
+ };
2069
2253
  }
2070
- }
2071
- const handlerResult = action.handler ? await action.handler({
2072
- actionInvocationId,
2073
- tenantId,
2074
- spaceId,
2075
- actorId: invocation.actorId,
2076
- actorType: invocation.actorType,
2077
- correlationId: invocation.correlationId,
2078
- ...invocation.causationId ? { causationId: invocation.causationId } : {},
2079
- ...invocation.provenance ? { provenance: invocation.provenance } : {},
2080
- ...isAdmissionTime(invocation.admittedAt) ? { admittedAt: invocation.admittedAt } : {},
2081
- db,
2082
- services: options.services
2083
- }, parsed.data) : {
2084
- success: true,
2085
- data: {}
2086
- };
2087
- if (!handlerResult.success) throw handlerResult.error != null ? new Error(handlerResult.error) : new HostError("Action handler failed");
2088
- const handlerData = handlerResult.data ?? {};
2089
- if (action.resultSchema) {
2090
- const publicResult = withoutPrivateHostFields(handlerData, eventResultFields);
2091
- const parsedResult = action.resultSchema.safeParse(publicResult);
2092
- if (!parsedResult.success) throw new HostError(`Action result validation failed at ${parsedResult.error.issues.map((issue) => issue.path.map(String).join(".") || "result").join(", ")}`);
2093
- }
2094
- const events = extractEvents(handlerData);
2095
- const undeclaredEvent = events.find((event) => !action.emitsEvents.includes(event.eventType));
2096
- if (undeclaredEvent) throw new HostError(`${action.actionId} emitted undeclared event type ${undeclaredEvent.eventType}`);
2097
- if (action.eventPhase !== "after_adapters") for (const [index, event] of events.entries()) await hostOperation(() => appendEvent(invocation, event, `domain:${index}`, action.version, transaction));
2098
- let transitionEvent;
2099
- if (binding && validatedTransition?.entityId && (validatedTransition.valid || replayedTransition)) {
2100
- const { entityId, currentState, targetState } = validatedTransition;
2101
- if (!events.some((event) => event.eventType === "StateTransitioned" && event.subjectType === binding.entityType && event.subjectId === entityId)) transitionEvent = {
2102
- eventType: "StateTransitioned",
2103
- subjectType: binding.entityType,
2104
- subjectId: entityId,
2105
- payload: {
2106
- entityType: binding.entityType,
2107
- entityId,
2108
- fromState: replayedTransition?.fromState ?? currentState,
2109
- toState: targetState,
2110
- causedByAction: action.actionId
2254
+ if (transitionEvent && action.eventPhase !== "after_adapters") await hostOperation(() => appendEvent(invocation, transitionEvent, "state:transition", 1, transaction));
2255
+ if (transaction && pendingStateTransition) await hostOperation(() => transaction.updateActionInvocation(actionInvocationId, tenantId, spaceId, { pendingStateTransition }));
2256
+ if (preparedFinalizationInput) {
2257
+ const skipped = [];
2258
+ for (const [index, step] of (action.adapterSteps ?? []).entries()) {
2259
+ const input = step.getInput(structuredClone(preparedFinalizationInput.params), structuredClone(preparedFinalizationInput.handlerData));
2260
+ preparedInputs.set(index, input);
2261
+ if (!input) skipped.push(index);
2111
2262
  }
2263
+ preparation = normalizeFinalizationJson({
2264
+ protocol: "atomic-after-adapters-v1",
2265
+ actionVersion: action.version,
2266
+ steps: (action.adapterSteps ?? []).map(({ adapterType, operation }) => ({
2267
+ adapterType,
2268
+ operation
2269
+ })),
2270
+ ...preparedFinalizationInput,
2271
+ handlerEvents: events,
2272
+ result: preparedFinalizationInput.handlerData,
2273
+ ...transitionEvent ? { stateTransition: transitionEvent } : {},
2274
+ skipped
2275
+ });
2276
+ if (new TextEncoder().encode((0, _fabricorg_platform.canonicalJson)(preparation)).byteLength > 262144) throw new HostError("Finalization checkpoint exceeds 256 KiB.");
2277
+ if (executionLease) {
2278
+ if (!await transaction.updateLeasedActionInvocation({
2279
+ id: actionInvocationId,
2280
+ tenantId,
2281
+ spaceId,
2282
+ workerId: executionLease.leaseOwner,
2283
+ leaseToken: executionLease.leaseToken,
2284
+ patch: { finalizationCheckpoint: preparation }
2285
+ })) throw new RecoverableFinalizationError(`Invocation lease lost: ${actionInvocationId}`);
2286
+ } else await transaction.updateActionInvocation(actionInvocationId, tenantId, spaceId, { finalizationCheckpoint: preparation });
2287
+ }
2288
+ return {
2289
+ data: handlerData,
2290
+ domainEvents: events,
2291
+ pendingTransitionEvent: transitionEvent,
2292
+ finalizationInput: preparedFinalizationInput
2112
2293
  };
2113
- }
2114
- if (transitionEvent && action.eventPhase !== "after_adapters") await hostOperation(() => appendEvent(invocation, transitionEvent, "state:transition", 1, transaction));
2115
- if (transaction && pendingStateTransition) await hostOperation(() => transaction.updateActionInvocation(actionInvocationId, tenantId, spaceId, { pendingStateTransition }));
2116
- return {
2117
- data: handlerData,
2118
- domainEvents: events,
2119
- pendingTransitionEvent: transitionEvent
2120
2294
  };
2121
- };
2122
- const atomicStore = asAtomicMutationStore(options.store);
2123
- let callbackFailure;
2124
- const run = async (db, transaction) => {
2125
- callbackFailure = void 0;
2126
- try {
2127
- return await runHandler(db, transaction);
2128
- } catch (error) {
2129
- callbackFailure = { error };
2130
- throw error;
2131
- }
2132
- };
2133
- const executed = await (async () => {
2134
- try {
2135
- return atomicStore ? await atomicStore.transactionWithEvents((transaction) => run(transaction.db, transaction)) : await options.store.transaction((db) => run(db));
2136
- } catch (error) {
2137
- if (callbackFailure) throw callbackFailure.error;
2138
- return hostOperation(() => Promise.reject(error));
2139
- }
2140
- })();
2141
- data = executed.data;
2142
- domainEvents = executed.domainEvents;
2143
- pendingTransitionEvent = executed.pendingTransitionEvent;
2295
+ const atomicStore = asAtomicMutationStore(options.store);
2296
+ let callbackFailure;
2297
+ const run = async (db, transaction) => {
2298
+ callbackFailure = void 0;
2299
+ try {
2300
+ return await runHandler(db, transaction);
2301
+ } catch (error) {
2302
+ callbackFailure = { error };
2303
+ throw error;
2304
+ }
2305
+ };
2306
+ const executed = await (async () => {
2307
+ try {
2308
+ return atomicStore ? await atomicStore.transactionWithEvents((transaction) => run(transaction.db, transaction)) : await options.store.transaction((db) => run(db));
2309
+ } catch (error) {
2310
+ if (callbackFailure) throw callbackFailure.error;
2311
+ if (action.finalize) throw new RecoverableFinalizationError(caughtError({
2312
+ actionId: action.actionId,
2313
+ source: "execution"
2314
+ }, error));
2315
+ return hostOperation(() => Promise.reject(error));
2316
+ }
2317
+ })();
2318
+ data = executed.data;
2319
+ finalizationInput = executed.finalizationInput;
2320
+ domainEvents = executed.domainEvents;
2321
+ pendingTransitionEvent = executed.pendingTransitionEvent;
2322
+ if (preparation) invocation = {
2323
+ ...invocation,
2324
+ finalizationCheckpoint: preparation
2325
+ };
2326
+ }
2144
2327
  } catch (error) {
2145
2328
  if (error instanceof RecoverableFinalizationError) throw error;
2146
2329
  if ((0, _fabricorg_platform.isReconciliationRequiredError)(error)) {
@@ -2165,6 +2348,7 @@ function createGovernedActionHost(options) {
2165
2348
  source: "handler"
2166
2349
  }, error)));
2167
2350
  }
2351
+ const finalizationSteps = [];
2168
2352
  let pendingHandoff = invocation.pendingCompletion;
2169
2353
  for (const [stepIndex, step] of (action.adapterSteps ?? []).entries()) {
2170
2354
  const adapterInvocationId = lifecycleId("adp", actionInvocationId, String(stepIndex));
@@ -2234,8 +2418,16 @@ function createGovernedActionHost(options) {
2234
2418
  }, error));
2235
2419
  }
2236
2420
  if (previousAdapterInvocation?.status === "ambiguous") return await settleUnknownOutcome(stepIndex, recordedAdapterReconciliation(previousAdapterInvocation), previousAdapterInvocation.reconciliation?.adapterEventSubject);
2237
- const input = step.getInput(parsed.data, data);
2238
- if (!input) continue;
2421
+ const input = action.finalize ? preparation.skipped.includes(stepIndex) ? void 0 : replayCheckpoint ? step.getInput(structuredClone(replayCheckpoint.params), structuredClone(replayCheckpoint.handlerData)) : preparedInputs.get(stepIndex) : step.getInput(parsed.data, data);
2422
+ if (!input) {
2423
+ if (action.finalize) finalizationSteps.push({
2424
+ stepIndex,
2425
+ adapterType: step.adapterType,
2426
+ operation: step.operation,
2427
+ status: "skipped"
2428
+ });
2429
+ continue;
2430
+ }
2239
2431
  const adapter = adapters.require(step.adapterType, step.operation);
2240
2432
  const adapterEventSubject = options.adapterEventSubject?.(action.actionId, parsed.data, data) ?? {
2241
2433
  subjectType: "AdapterInvocation",
@@ -2391,8 +2583,9 @@ function createGovernedActionHost(options) {
2391
2583
  let thrownOutcome;
2392
2584
  let adapterDeadlineTimer;
2393
2585
  try {
2394
- const adapterController = options.adapterDeadlineMs ? new AbortController() : void 0;
2395
- const deadlineMs = options.adapterDeadlineMs ? now().getTime() + options.adapterDeadlineMs : void 0;
2586
+ const effectiveDeadlineMs = step.deadlineMs ?? options.adapterDeadlineMs;
2587
+ const adapterController = effectiveDeadlineMs ? new AbortController() : void 0;
2588
+ const deadlineMs = effectiveDeadlineMs ? now().getTime() + effectiveDeadlineMs : void 0;
2396
2589
  if (adapterController && deadlineMs) {
2397
2590
  const remaining = deadlineMs - now().getTime();
2398
2591
  if (remaining <= 0) throw new HostError(`Adapter deadline already expired before execution: ${step.adapterType}:${step.operation}`);
@@ -2577,6 +2770,7 @@ function createGovernedActionHost(options) {
2577
2770
  ...pendingCompletion ? { pendingCompletion } : {}
2578
2771
  }, action.actionId);
2579
2772
  }
2773
+ if (action.finalize) output = normalizeFinalizationJson(output);
2580
2774
  successOutput = output;
2581
2775
  if (handoff) {
2582
2776
  const reference = typeof handoff.externalReference === "string" ? handoff.externalReference.trim() : "";
@@ -2810,7 +3004,8 @@ function createGovernedActionHost(options) {
2810
3004
  return hostOperation(() => failExecution(invocation, "failed", message));
2811
3005
  }
2812
3006
  }
2813
- const result = withoutPrivateHostFields(data, eventResultFields);
3007
+ if (action.finalize && pendingHandoff) return settleAppliedEffect("Finalizing actions cannot finalize accepted external operations.", pendingHandoff);
3008
+ let result = action.finalize ? { ...replayCheckpoint?.result ?? finalizationInput.handlerData } : withoutPrivateHostFields(data, eventResultFields);
2814
3009
  const parked = pendingHandoff ? {
2815
3010
  ...pendingHandoff,
2816
3011
  parkedAt: now()
@@ -2824,25 +3019,83 @@ function createGovernedActionHost(options) {
2824
3019
  result
2825
3020
  };
2826
3021
  let verdictWritten = true;
3022
+ let finalizationCallbackFailure;
2827
3023
  try {
2828
3024
  const atomicStore = asAtomicMutationStore(options.store);
2829
3025
  if (action.eventPhase === "after_adapters" && atomicStore) {
2830
- await hostOperation(() => atomicStore.transactionWithEvents(async (transaction) => {
2831
- for (const [index, event] of domainEvents.entries()) await appendEvent(invocation, event, `domain:${index}`, action.version, transaction);
2832
- if (pendingTransitionEvent) await appendEvent(invocation, pendingTransitionEvent, "state:transition", 1, transaction);
2833
- if (executionLease) {
2834
- if (!transaction.updateLeasedActionInvocation) throw new HostError("Leased atomic finalization requires transaction-scoped fencing support.");
2835
- if (!await transaction.updateLeasedActionInvocation({
2836
- id: actionInvocationId,
2837
- tenantId,
2838
- spaceId,
2839
- workerId: executionLease.leaseOwner,
2840
- leaseToken: executionLease.leaseToken,
2841
- patch: finalPatch
2842
- })) throw new RecoverableFinalizationError(`Invocation lease lost: ${actionInvocationId}`);
2843
- } else verdictWritten = await writeInlineInvocation(invocation, finalPatch, transaction, onPreservedReconciliation);
2844
- }));
2845
- if (!pendingHandoff && verdictWritten) emitInvocationStatusTelemetry(invocation, "completed", options.telemetry, now());
3026
+ if (action.finalize) for (const [stepIndex, step] of (action.adapterSteps ?? []).entries()) {
3027
+ if (finalizationSteps.some((entry) => entry.stepIndex === stepIndex)) continue;
3028
+ const row = await options.store.getAdapterInvocation(lifecycleId("adp", actionInvocationId, String(stepIndex)), adapterInvocationScope);
3029
+ if (row?.status !== "succeeded" || row.output === void 0) {
3030
+ const error = /* @__PURE__ */ new Error("Finalization requires successful stored adapter output.");
3031
+ finalizationCallbackFailure = { error };
3032
+ throw error;
3033
+ }
3034
+ finalizationSteps.push({
3035
+ stepIndex,
3036
+ adapterType: step.adapterType,
3037
+ operation: step.operation,
3038
+ status: "succeeded",
3039
+ adapterInvocationId: row.id,
3040
+ output: finalizationJson(row.output)
3041
+ });
3042
+ }
3043
+ const finalizeSuccess = () => atomicStore.transactionWithEvents(async (transaction) => {
3044
+ try {
3045
+ let finalized;
3046
+ async function assertFinalizationOwner() {
3047
+ const current = await transaction.getActionInvocationForUpdate(actionInvocationId, tenantId, spaceId);
3048
+ if (!current || current.status !== "running" || (executionLease ? current.leaseOwner !== executionLease.leaseOwner || current.leaseToken !== executionLease.leaseToken : !!current.leaseOwner)) throw new RecoverableFinalizationError(`Invocation lease lost or no longer running: ${actionInvocationId}`);
3049
+ }
3050
+ if (action.finalize) {
3051
+ await assertFinalizationOwner();
3052
+ finalized = validateFinalization(await action.finalize({
3053
+ actionInvocationId,
3054
+ tenantId,
3055
+ spaceId,
3056
+ actorId: invocation.actorId,
3057
+ actorType: invocation.actorType,
3058
+ correlationId: invocation.correlationId,
3059
+ causationId: invocation.causationId,
3060
+ provenance: invocation.provenance ? structuredClone(invocation.provenance) : void 0,
3061
+ db: transaction.db
3062
+ }, {
3063
+ ...structuredClone(finalizationInput),
3064
+ adapterOutputs: structuredClone(finalizationSteps.sort((a, b) => a.stepIndex - b.stepIndex))
3065
+ }), action, eventResultFields, options.eventContracts);
3066
+ result = withoutPrivateHostFields({
3067
+ ...result,
3068
+ ...finalized.resultPatch
3069
+ }, eventResultFields);
3070
+ if (action.resultSchema && !action.resultSchema.safeParse(result).success) throw new Error("Finalization merged result does not match resultSchema.");
3071
+ finalPatch.result = result;
3072
+ }
3073
+ for (const [index, event] of domainEvents.entries()) await appendEvent(invocation, event, `domain:${index}`, action.version, transaction);
3074
+ for (const [index, event] of (finalized?.events ?? []).entries()) await appendEvent(invocation, event, `finalize:${index}`, action.version, transaction);
3075
+ if (pendingTransitionEvent) await appendEvent(invocation, pendingTransitionEvent, "state:transition", 1, transaction);
3076
+ if (action.finalize) await assertFinalizationOwner();
3077
+ if (executionLease) {
3078
+ if (!transaction.updateLeasedActionInvocation) throw new HostError("Leased atomic finalization requires transaction-scoped fencing support.");
3079
+ if (!await transaction.updateLeasedActionInvocation({
3080
+ id: actionInvocationId,
3081
+ tenantId,
3082
+ spaceId,
3083
+ workerId: executionLease.leaseOwner,
3084
+ leaseToken: executionLease.leaseToken,
3085
+ patch: finalPatch
3086
+ })) throw new RecoverableFinalizationError(`Invocation lease lost: ${actionInvocationId}`);
3087
+ } else verdictWritten = await writeInlineInvocation(invocation, finalPatch, transaction, onPreservedReconciliation);
3088
+ } catch (error) {
3089
+ finalizationCallbackFailure = { error };
3090
+ throw error;
3091
+ }
3092
+ });
3093
+ if (action.finalize) await finalizeSuccess();
3094
+ else await hostOperation(finalizeSuccess);
3095
+ if (!pendingHandoff && verdictWritten) {
3096
+ if (action.finalize) rememberVerdict(finalPatch);
3097
+ emitInvocationStatusTelemetry(invocation, "completed", options.telemetry, now());
3098
+ }
2846
3099
  } else {
2847
3100
  if (action.eventPhase === "after_adapters") {
2848
3101
  for (const [index, event] of domainEvents.entries()) await hostOperation(() => appendEvent(invocation, event, `domain:${index}`, action.version));
@@ -2851,6 +3104,18 @@ function createGovernedActionHost(options) {
2851
3104
  await hostOperation(() => persistExecutionInvocation(invocation, finalPatch));
2852
3105
  }
2853
3106
  } catch (error) {
3107
+ if (action.finalize) {
3108
+ if (error instanceof RecoverableFinalizationError) throw error;
3109
+ if (!finalizationCallbackFailure || finalizationCallbackFailure.error !== error) throw new RecoverableFinalizationError(caughtError({
3110
+ actionId: action.actionId,
3111
+ source: "execution"
3112
+ }, error));
3113
+ const message = caughtError({
3114
+ actionId: action.actionId,
3115
+ source: "execution"
3116
+ }, error);
3117
+ return hasDurableProviderEffect ? settleAppliedEffect(message) : hostOperation(() => failExecution(invocation, "failed", message));
3118
+ }
2854
3119
  if (action.eventPhase === "after_adapters") throw new RecoverableFinalizationError(errorMessage(error));
2855
3120
  throw error;
2856
3121
  }
@@ -2865,7 +3130,13 @@ function createGovernedActionHost(options) {
2865
3130
  } catch (error) {
2866
3131
  if (error instanceof RecoverableFinalizationError) throw error;
2867
3132
  if (retryableGovernance && (0, _fabricorg_platform.isRetryableExecutionError)(error)) {
2868
- const reason = executionReasonOverride === "approval_resume" ? "approval resume" : !invocation.leaseOwner || invocation.leaseToken === void 0 ? "not leased" : !action.idempotent ? "action not idempotent" : invocation.attemptCount >= retryableExecutionMaxAttempts ? `attempts exhausted (${invocation.attemptCount}/${retryableExecutionMaxAttempts})` : void 0;
3133
+ let reason = executionReasonOverride === "approval_resume" ? "approval resume" : !invocation.leaseOwner || invocation.leaseToken === void 0 ? "not leased" : invocation.attemptCount >= retryableExecutionMaxAttempts ? `attempts exhausted (${invocation.attemptCount}/${retryableExecutionMaxAttempts})` : void 0;
3134
+ if (!reason && !action.idempotent) try {
3135
+ await persistExecutionInvocation(invocation, { deferredBeforeEffectAttempt: invocation.attemptCount });
3136
+ } catch (markerError) {
3137
+ if (markerError instanceof RecoverableFinalizationError) throw markerError;
3138
+ reason = "pre-effect marker could not be recorded";
3139
+ }
2869
3140
  if (!reason) throw error;
2870
3141
  return hostOperation(() => failExecution(invocation, "failed", caughtError({
2871
3142
  actionId: invocation.actionId,
@@ -3671,6 +3942,11 @@ var MemoryPlatformHostStore = class {
3671
3942
  const record = this.invocations.find((row) => row.id === id && row.tenantId === tenantId && row.spaceId === spaceId);
3672
3943
  if (!record) throw new Error(`ActionInvocation not found: ${id}`);
3673
3944
  Object.assign(record, patch, { updatedAt: /* @__PURE__ */ new Date() });
3945
+ if (Object.hasOwn(patch, "finalizationCheckpoint")) {
3946
+ if (patch.finalizationCheckpoint) record.finalizationCheckpoint = structuredClone(patch.finalizationCheckpoint);
3947
+ else delete record.finalizationCheckpoint;
3948
+ }
3949
+ if (Object.hasOwn(patch, "deferredBeforeEffectAttempt") && patch.deferredBeforeEffectAttempt === void 0) delete record.deferredBeforeEffectAttempt;
3674
3950
  if (Object.hasOwn(patch, "pendingStateTransition")) {
3675
3951
  if (patch.pendingStateTransition) record.pendingStateTransition = { ...patch.pendingStateTransition };
3676
3952
  else delete record.pendingStateTransition;
@@ -4307,6 +4583,16 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
4307
4583
  sql: `
4308
4584
  ALTER TABLE fabric_platform.adapter_invocations ADD COLUMN IF NOT EXISTS reconciliation jsonb;
4309
4585
  `
4586
+ },
4587
+ {
4588
+ version: 9,
4589
+ name: "pre_effect_governance_deferral",
4590
+ sql: `ALTER TABLE fabric_platform.action_invocations ADD COLUMN IF NOT EXISTS deferred_before_effect_attempt integer;`
4591
+ },
4592
+ {
4593
+ version: 10,
4594
+ name: "finalization_preparation_checkpoint",
4595
+ sql: `ALTER TABLE fabric_platform.action_invocations ADD COLUMN IF NOT EXISTS finalization_checkpoint jsonb;`
4310
4596
  }
4311
4597
  ]);
4312
4598
  }
@@ -4380,6 +4666,8 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
4380
4666
  pending_completion=CASE WHEN $12::boolean THEN $13::jsonb ELSE pending_completion END,
4381
4667
  completion_due_at=CASE WHEN $12::boolean THEN ($13::jsonb->>'dueAt')::timestamptz ELSE completion_due_at END,
4382
4668
  external_completion=CASE WHEN $14::boolean THEN $15::jsonb ELSE external_completion END,
4669
+ ${Object.hasOwn(patch, "finalizationCheckpoint") ? `finalization_checkpoint=$${18 + (Object.hasOwn(patch, "deferredBeforeEffectAttempt") ? 2 : 0)}::jsonb,` : ""}
4670
+ ${Object.hasOwn(patch, "deferredBeforeEffectAttempt") ? "deferred_before_effect_attempt=CASE WHEN $18::boolean THEN $19::integer ELSE deferred_before_effect_attempt END," : ""}
4383
4671
  pending_state_transition=CASE WHEN $16::boolean THEN $17::jsonb ELSE pending_state_transition END,
4384
4672
  lease_owner=CASE WHEN ($13::jsonb IS NOT NULL AND $4='running') OR $4 IN ('waiting_for_approval','completed','failed','blocked_by_policy','reconciliation_required','validation_failed')
4385
4673
  THEN NULL ELSE lease_owner END,
@@ -4403,7 +4691,9 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
4403
4691
  Object.hasOwn(patch, "externalCompletion"),
4404
4692
  patch.externalCompletion ? JSON.stringify(patch.externalCompletion) : null,
4405
4693
  Object.hasOwn(patch, "pendingStateTransition"),
4406
- patch.pendingStateTransition ? JSON.stringify(patch.pendingStateTransition) : null
4694
+ patch.pendingStateTransition ? JSON.stringify(patch.pendingStateTransition) : null,
4695
+ ...Object.hasOwn(patch, "deferredBeforeEffectAttempt") ? [true, patch.deferredBeforeEffectAttempt ?? null] : [],
4696
+ ...Object.hasOwn(patch, "finalizationCheckpoint") ? [patch.finalizationCheckpoint ? JSON.stringify(patch.finalizationCheckpoint) : null] : []
4407
4697
  ]);
4408
4698
  }
4409
4699
  async recordHitlDecision(actionInvocationId, tenantId, spaceId, decision) {
@@ -5033,6 +5323,8 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
5033
5323
  pending_completion=CASE WHEN $14::boolean THEN $15::jsonb ELSE pending_completion END,
5034
5324
  completion_due_at=CASE WHEN $14::boolean THEN ($15::jsonb->>'dueAt')::timestamptz ELSE completion_due_at END,
5035
5325
  external_completion=CASE WHEN $16::boolean THEN $17::jsonb ELSE external_completion END,
5326
+ ${Object.hasOwn(patch, "finalizationCheckpoint") ? `finalization_checkpoint=$${20 + (Object.hasOwn(patch, "deferredBeforeEffectAttempt") ? 2 : 0)}::jsonb,` : ""}
5327
+ ${Object.hasOwn(patch, "deferredBeforeEffectAttempt") ? "deferred_before_effect_attempt=CASE WHEN $20::boolean THEN $21::integer ELSE deferred_before_effect_attempt END," : ""}
5036
5328
  pending_state_transition=CASE WHEN $18::boolean THEN $19::jsonb ELSE pending_state_transition END,
5037
5329
  lease_owner=CASE WHEN ($15::jsonb IS NOT NULL AND $6='running') OR $6 IN ('waiting_for_approval','completed','failed','blocked_by_policy','reconciliation_required','validation_failed')
5038
5330
  THEN NULL ELSE lease_owner END,
@@ -5060,7 +5352,9 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
5060
5352
  Object.hasOwn(patch, "externalCompletion"),
5061
5353
  patch.externalCompletion ? JSON.stringify(patch.externalCompletion) : null,
5062
5354
  Object.hasOwn(patch, "pendingStateTransition"),
5063
- patch.pendingStateTransition ? JSON.stringify(patch.pendingStateTransition) : null
5355
+ patch.pendingStateTransition ? JSON.stringify(patch.pendingStateTransition) : null,
5356
+ ...Object.hasOwn(patch, "deferredBeforeEffectAttempt") ? [true, patch.deferredBeforeEffectAttempt ?? null] : [],
5357
+ ...Object.hasOwn(patch, "finalizationCheckpoint") ? [patch.finalizationCheckpoint ? JSON.stringify(patch.finalizationCheckpoint) : null] : []
5064
5358
  ])).rows.length === 1;
5065
5359
  }
5066
5360
  async renewActionInvocationLease(input) {
@@ -5152,11 +5446,13 @@ function toActionRecord(row) {
5152
5446
  ...row.execution_reason ? { executionReason: String(row.execution_reason) } : {},
5153
5447
  ...row.authorization_reconciliation ? { authorizationReconciliation: row.authorization_reconciliation } : {},
5154
5448
  ...row.adapter_reconciliation ? { adapterReconciliation: row.adapter_reconciliation } : {},
5449
+ ...row.finalization_checkpoint ? { finalizationCheckpoint: row.finalization_checkpoint } : {},
5155
5450
  ...row.pending_state_transition ? { pendingStateTransition: row.pending_state_transition } : {},
5156
5451
  ...row.pending_completion ? { pendingCompletion: toPendingCompletion(row.pending_completion) } : {},
5157
5452
  ...row.external_completion ? { externalCompletion: toExternalCompletion(row.external_completion) } : {},
5158
5453
  ...row.authorization_binding_id ? { authorizationBindingId: String(row.authorization_binding_id) } : {},
5159
5454
  attemptCount: Number(row.attempt_count ?? 0),
5455
+ ...row.deferred_before_effect_attempt == null ? {} : { deferredBeforeEffectAttempt: Number(row.deferred_before_effect_attempt) },
5160
5456
  leaseToken: Number(row.lease_token ?? 0),
5161
5457
  ...row.lease_owner ? { leaseOwner: String(row.lease_owner) } : {},
5162
5458
  ...row.lease_expires_at ? { leaseExpiresAt: new Date(row.lease_expires_at) } : {},