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