@fabricorg/platform-host 7.5.0 → 7.6.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.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { ActorType, ActionId, ActionStatus, EvidencePacketReference, AgentActionRoute, AgentActionRiskTier, MutationFootprint, ExecutionPrincipal, GovernanceRuntimeEvidence, PolicyOutcome, AssetEventEnvelope, ExecutionAttestation, ExternalReconciliation, PolicyObligation, PolicyObligationStatus, ModuleRegistry, ActionDefinition, AdapterImplementation, AdapterCircuitBreakerProvider, FabricRuntimeServices, AgentActionPolicyEvaluator, MutationGovernanceResolver, RuntimePolicyDefinition } from '@fabricorg/platform';
1
+ import { ActorType, ActionId, ActionStatus, EvidencePacketReference, AgentActionRoute, AgentActionRiskTier, MutationFootprint, ExecutionPrincipal, GovernanceRuntimeEvidence, PolicyOutcome, AssetEventEnvelope, PolicyObligation, PolicyObligationStatus, ExecutionAttestation, ExternalReconciliation, ModuleRegistry, ActionDefinition, AdapterImplementation, AdapterCircuitBreakerProvider, FabricRuntimeServices, AgentActionPolicyEvaluator, MutationGovernanceResolver, RuntimePolicyDefinition } from '@fabricorg/platform';
2
2
  export { canonicalJson } from '@fabricorg/platform';
3
3
  import { AssemblyLockfile } from '@fabricorg/assembly';
4
4
 
@@ -264,6 +264,18 @@ interface ActionInvocationRecord {
264
264
  authorizationReconciliation?: AuthorizationReconciliationOutcome;
265
265
  /** Durable evidence when an adapter returned an ambiguous outcome requiring reconciliation. */
266
266
  adapterReconciliation?: AdapterReconciliationOutcome;
267
+ /**
268
+ * Transition Host validated for an idempotent after_adapters action, saved
269
+ * after handler/result/event validation inside a mutation transaction, or before
270
+ * the handler otherwise. Supports recovered StateTransitioned emission and is
271
+ * retained as evidence after completion.
272
+ */
273
+ pendingStateTransition?: {
274
+ entityType: string;
275
+ entityId: string;
276
+ fromState: string;
277
+ toState: string;
278
+ };
267
279
  /**
268
280
  * A handoff the host is waiting on. While present the invocation is
269
281
  * `running` without a lease: it is not stuck, it is somebody else's turn,
@@ -518,6 +530,19 @@ interface PlatformHostMutationTransaction<TDb> {
518
530
  getEntityState?(tenantId: string, spaceId: string, entityType: string, entityId: string): Promise<string | undefined>;
519
531
  updateActionInvocation(id: string, tenantId: string, spaceId: string, patch: ActionInvocationPatch): Promise<void>;
520
532
  updateLeasedActionInvocation?(input: UpdateLeasedActionInvocationInput): Promise<boolean>;
533
+ /**
534
+ * Update an adapter row in this transaction, with the same optional
535
+ * tenant/space/invocation scope semantics as the store method. Host uses
536
+ * this to commit degraded adapter evidence and its terminal verdict together.
537
+ * Omit this member if the adapter row cannot share this transaction.
538
+ */
539
+ updateAdapterInvocation?: PlatformHostStore<TDb>["updateAdapterInvocation"];
540
+ /**
541
+ * Append governance reconciliation evidence in this transaction. Host uses
542
+ * this for degraded finalization when adapter output is withheld. Omit this
543
+ * member if governance evidence cannot share the event/verdict transaction.
544
+ */
545
+ appendExternalReconciliation?: GovernancePlatformHostStore<TDb>["appendExternalReconciliation"];
521
546
  }
522
547
  type OutboxStatus = "pending" | "published" | "dead_letter";
523
548
  type EventPayloadClassification = "public" | "internal" | "confidential" | "restricted";
@@ -819,7 +844,7 @@ interface ClaimActionInvocationsInput {
819
844
  * - `claimActionInvocations` must never claim a running invocation without a
820
845
  * lease, which is what a parked invocation is.
821
846
  */
822
- type ActionInvocationPatch = Partial<Pick<ActionInvocationRecord, "status" | "result" | "error" | "authorizationReconciliation" | "adapterReconciliation" | "pendingCompletion" | "externalCompletion">>;
847
+ type ActionInvocationPatch = Partial<Pick<ActionInvocationRecord, "status" | "result" | "error" | "authorizationReconciliation" | "adapterReconciliation" | "pendingStateTransition" | "pendingCompletion" | "externalCompletion">>;
823
848
  interface UpdateLeasedActionInvocationInput {
824
849
  id: string;
825
850
  tenantId: string;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ActorType, ActionId, ActionStatus, EvidencePacketReference, AgentActionRoute, AgentActionRiskTier, MutationFootprint, ExecutionPrincipal, GovernanceRuntimeEvidence, PolicyOutcome, AssetEventEnvelope, ExecutionAttestation, ExternalReconciliation, PolicyObligation, PolicyObligationStatus, ModuleRegistry, ActionDefinition, AdapterImplementation, AdapterCircuitBreakerProvider, FabricRuntimeServices, AgentActionPolicyEvaluator, MutationGovernanceResolver, RuntimePolicyDefinition } from '@fabricorg/platform';
1
+ import { ActorType, ActionId, ActionStatus, EvidencePacketReference, AgentActionRoute, AgentActionRiskTier, MutationFootprint, ExecutionPrincipal, GovernanceRuntimeEvidence, PolicyOutcome, AssetEventEnvelope, PolicyObligation, PolicyObligationStatus, ExecutionAttestation, ExternalReconciliation, ModuleRegistry, ActionDefinition, AdapterImplementation, AdapterCircuitBreakerProvider, FabricRuntimeServices, AgentActionPolicyEvaluator, MutationGovernanceResolver, RuntimePolicyDefinition } from '@fabricorg/platform';
2
2
  export { canonicalJson } from '@fabricorg/platform';
3
3
  import { AssemblyLockfile } from '@fabricorg/assembly';
4
4
 
@@ -264,6 +264,18 @@ interface ActionInvocationRecord {
264
264
  authorizationReconciliation?: AuthorizationReconciliationOutcome;
265
265
  /** Durable evidence when an adapter returned an ambiguous outcome requiring reconciliation. */
266
266
  adapterReconciliation?: AdapterReconciliationOutcome;
267
+ /**
268
+ * Transition Host validated for an idempotent after_adapters action, saved
269
+ * after handler/result/event validation inside a mutation transaction, or before
270
+ * the handler otherwise. Supports recovered StateTransitioned emission and is
271
+ * retained as evidence after completion.
272
+ */
273
+ pendingStateTransition?: {
274
+ entityType: string;
275
+ entityId: string;
276
+ fromState: string;
277
+ toState: string;
278
+ };
267
279
  /**
268
280
  * A handoff the host is waiting on. While present the invocation is
269
281
  * `running` without a lease: it is not stuck, it is somebody else's turn,
@@ -518,6 +530,19 @@ interface PlatformHostMutationTransaction<TDb> {
518
530
  getEntityState?(tenantId: string, spaceId: string, entityType: string, entityId: string): Promise<string | undefined>;
519
531
  updateActionInvocation(id: string, tenantId: string, spaceId: string, patch: ActionInvocationPatch): Promise<void>;
520
532
  updateLeasedActionInvocation?(input: UpdateLeasedActionInvocationInput): Promise<boolean>;
533
+ /**
534
+ * Update an adapter row in this transaction, with the same optional
535
+ * tenant/space/invocation scope semantics as the store method. Host uses
536
+ * this to commit degraded adapter evidence and its terminal verdict together.
537
+ * Omit this member if the adapter row cannot share this transaction.
538
+ */
539
+ updateAdapterInvocation?: PlatformHostStore<TDb>["updateAdapterInvocation"];
540
+ /**
541
+ * Append governance reconciliation evidence in this transaction. Host uses
542
+ * this for degraded finalization when adapter output is withheld. Omit this
543
+ * member if governance evidence cannot share the event/verdict transaction.
544
+ */
545
+ appendExternalReconciliation?: GovernancePlatformHostStore<TDb>["appendExternalReconciliation"];
521
546
  }
522
547
  type OutboxStatus = "pending" | "published" | "dead_letter";
523
548
  type EventPayloadClassification = "public" | "internal" | "confidential" | "restricted";
@@ -819,7 +844,7 @@ interface ClaimActionInvocationsInput {
819
844
  * - `claimActionInvocations` must never claim a running invocation without a
820
845
  * lease, which is what a parked invocation is.
821
846
  */
822
- type ActionInvocationPatch = Partial<Pick<ActionInvocationRecord, "status" | "result" | "error" | "authorizationReconciliation" | "adapterReconciliation" | "pendingCompletion" | "externalCompletion">>;
847
+ type ActionInvocationPatch = Partial<Pick<ActionInvocationRecord, "status" | "result" | "error" | "authorizationReconciliation" | "adapterReconciliation" | "pendingStateTransition" | "pendingCompletion" | "externalCompletion">>;
823
848
  interface UpdateLeasedActionInvocationInput {
824
849
  id: string;
825
850
  tenantId: string;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { canonicalJson, AdapterRegistry, assertAdapterCircuitBreakerConfig, AdapterCircuitBreakerRegistry, isRetryableExecutionError, createFabricId, assertGovernanceRuntimeEvidence, FABRIC_GOVERNANCE_CONTRACT_VERSION, assertMutationGovernanceContext, evaluatePolicyDefinitions, aggregatePolicyOutcomes, evaluateStateMachineTransition, executeWithAdapterRetry, isAdapterCircuitOpenError, assertAdapterOutcomeConsistency } from '@fabricorg/platform';
1
+ import { canonicalJson, AdapterRegistry, assertAdapterCircuitBreakerConfig, AdapterCircuitBreakerRegistry, isRetryableExecutionError, createFabricId, assertGovernanceRuntimeEvidence, FABRIC_GOVERNANCE_CONTRACT_VERSION, assertMutationGovernanceContext, evaluatePolicyDefinitions, aggregatePolicyOutcomes, evaluateStateMachineTransition, isReconciliationRequiredError, executeWithAdapterRetry, isAdapterCircuitOpenError, assertAdapterOutcomeConsistency } from '@fabricorg/platform';
2
2
  export { canonicalJson } from '@fabricorg/platform';
3
3
  import { performance } from 'perf_hooks';
4
4
  import { assertAssemblyRuntimeCompatible } from '@fabricorg/assembly';
@@ -1447,6 +1447,8 @@ function createGovernedActionHost(options) {
1447
1447
  throw new HostError("Outbox egress requires transactionWithEvents to provide appendEventWithOutbox before handler execution.");
1448
1448
  }
1449
1449
  const binding = action.stateMachine;
1450
+ let replayedTransition;
1451
+ let pendingStateTransition;
1450
1452
  let validatedTransition;
1451
1453
  if (binding) {
1452
1454
  const machine = stateMachineResolver(binding.entityType);
@@ -1499,6 +1501,21 @@ function createGovernedActionHost(options) {
1499
1501
  }
1500
1502
  replayingAppliedTransition = source?.actionInvocationId === actionInvocationId && source.state === currentState;
1501
1503
  }
1504
+ const pending = invocation.pendingStateTransition;
1505
+ if (replayingAppliedTransition && pending && pending.entityType === binding.entityType && pending.entityId === entityId && pending.toState === currentState) {
1506
+ replayedTransition = pending;
1507
+ }
1508
+ if (action.idempotent && transition.valid && entityId && action.eventPhase === "after_adapters") {
1509
+ pendingStateTransition = {
1510
+ entityType: binding.entityType,
1511
+ entityId,
1512
+ fromState: currentState,
1513
+ toState: targetState
1514
+ };
1515
+ if (!transaction) {
1516
+ await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, { pendingStateTransition });
1517
+ }
1518
+ }
1502
1519
  if (!transition.valid && !replayingAppliedTransition) {
1503
1520
  throw transition.error != null ? new Error(transition.error) : new HostError("Invalid state transition");
1504
1521
  }
@@ -1552,7 +1569,7 @@ function createGovernedActionHost(options) {
1552
1569
  }
1553
1570
  }
1554
1571
  let transitionEvent;
1555
- if (binding && validatedTransition?.valid && validatedTransition.entityId) {
1572
+ if (binding && validatedTransition?.entityId && (validatedTransition.valid || replayedTransition)) {
1556
1573
  const { entityId, currentState, targetState } = validatedTransition;
1557
1574
  const handlerEmittedTransition = events.some((event) => event.eventType === "StateTransitioned" && event.subjectType === binding.entityType && event.subjectId === entityId);
1558
1575
  if (!handlerEmittedTransition) {
@@ -1563,7 +1580,7 @@ function createGovernedActionHost(options) {
1563
1580
  payload: {
1564
1581
  entityType: binding.entityType,
1565
1582
  entityId,
1566
- fromState: currentState,
1583
+ fromState: replayedTransition?.fromState ?? currentState,
1567
1584
  toState: targetState,
1568
1585
  causedByAction: action.actionId
1569
1586
  }
@@ -1573,6 +1590,9 @@ function createGovernedActionHost(options) {
1573
1590
  if (transitionEvent && action.eventPhase !== "after_adapters") {
1574
1591
  await appendEvent(invocation, transitionEvent, "state:transition", 1, transaction);
1575
1592
  }
1593
+ if (transaction && pendingStateTransition) {
1594
+ await transaction.updateActionInvocation(actionInvocationId, tenantId, spaceId, { pendingStateTransition });
1595
+ }
1576
1596
  return { data: handlerData, domainEvents: events, pendingTransitionEvent: transitionEvent };
1577
1597
  };
1578
1598
  const atomicStore = asAtomicMutationStore(options.store);
@@ -1583,6 +1603,17 @@ function createGovernedActionHost(options) {
1583
1603
  domainEvents = executed.domainEvents;
1584
1604
  pendingTransitionEvent = executed.pendingTransitionEvent;
1585
1605
  } catch (error) {
1606
+ if (isReconciliationRequiredError(error)) {
1607
+ const message = caughtError({ actionId: invocation.actionId, source: "handler" }, error);
1608
+ await persistInvocation(invocation, { status: "reconciliation_required", error: message });
1609
+ return withConsistency({
1610
+ actionInvocationId,
1611
+ status: "reconciliation_required",
1612
+ error: message,
1613
+ ...invocation.hitlRoute ? { hitlRoute: invocation.hitlRoute } : {},
1614
+ ...invocation.hitlRiskTier ? { hitlRiskTier: invocation.hitlRiskTier } : {}
1615
+ }, action.actionId);
1616
+ }
1586
1617
  return fail2(invocation, "failed", caughtError({ actionId: invocation.actionId, source: "handler" }, error));
1587
1618
  }
1588
1619
  const adapterInvocationScope = { tenantId: invocation.tenantId, spaceId: invocation.spaceId, actionInvocationId: invocation.id };
@@ -1650,7 +1681,7 @@ function createGovernedActionHost(options) {
1650
1681
  subjectId: adapterEventSubject.subjectId,
1651
1682
  payload: { adapterType: step.adapterType, operation: step.operation }
1652
1683
  }, `adapter:${stepIndex}:started`);
1653
- async function appendDegradedAdapterEvidence(outputWithheld, message, accepted, output, externalReference) {
1684
+ async function appendDegradedAdapterEvidence(outputWithheld, message, accepted, output, externalReference, transaction) {
1654
1685
  if (accepted) {
1655
1686
  await appendEvent(invocation, {
1656
1687
  eventType: "ExternalOperationAccepted",
@@ -1662,7 +1693,7 @@ function createGovernedActionHost(options) {
1662
1693
  externalReference: accepted.externalReference,
1663
1694
  ...accepted.dueAt ? { dueAt: accepted.dueAt.toISOString() } : {}
1664
1695
  }
1665
- }, `adapter:${stepIndex}:accepted`);
1696
+ }, `adapter:${stepIndex}:accepted`, 1, transaction);
1666
1697
  }
1667
1698
  await appendEvent(invocation, {
1668
1699
  eventType: "AdapterInvocationSucceeded",
@@ -1674,10 +1705,10 @@ function createGovernedActionHost(options) {
1674
1705
  input: recordedInput,
1675
1706
  ...outputWithheld ? { outputWithheld: true } : { output }
1676
1707
  }
1677
- }, `adapter:${stepIndex}:succeeded`);
1708
+ }, `adapter:${stepIndex}:succeeded`, 1, transaction);
1678
1709
  const governanceStore2 = asGovernanceStore(options.store);
1679
1710
  if (outputWithheld && governanceStore2) {
1680
- await governanceStore2.appendExternalReconciliation({
1711
+ await (transaction ?? governanceStore2).appendExternalReconciliation({
1681
1712
  id: lifecycleId("rec", actionInvocationId, `${adapter.vendor}:${adapterInvocationId}`),
1682
1713
  actionInvocationId,
1683
1714
  tenantId,
@@ -1691,6 +1722,52 @@ function createGovernedActionHost(options) {
1691
1722
  });
1692
1723
  }
1693
1724
  }
1725
+ async function finalizeDegradedAdapterAtomically(patch, outputWithheld, message, accepted, output, externalReference) {
1726
+ const atomicStore = asAtomicMutationStore(options.store);
1727
+ if (!atomicStore) return false;
1728
+ const needsReconciliation = outputWithheld && !!asGovernanceStore(options.store);
1729
+ const previousStatus = invocation.status;
1730
+ const finalize = () => atomicStore.transactionWithEvents(async (transaction) => {
1731
+ if (!transaction.updateAdapterInvocation || needsReconciliation && !transaction.appendExternalReconciliation || invocation.leaseOwner && invocation.leaseToken !== void 0 && !transaction.updateLeasedActionInvocation) return false;
1732
+ await appendDegradedAdapterEvidence(outputWithheld, message, accepted, output, externalReference, transaction);
1733
+ await transaction.updateAdapterInvocation(adapterInvocationId, {
1734
+ status: "succeeded",
1735
+ ...outputWithheld ? {} : { output },
1736
+ updatedAt: now()
1737
+ }, adapterInvocationScope);
1738
+ if (invocation.leaseOwner && invocation.leaseToken !== void 0) {
1739
+ if (!transaction.updateLeasedActionInvocation) {
1740
+ throw new HostError("Leased atomic finalization requires transaction-scoped fencing support.");
1741
+ }
1742
+ const updated = await transaction.updateLeasedActionInvocation({
1743
+ id: actionInvocationId,
1744
+ tenantId,
1745
+ spaceId,
1746
+ workerId: invocation.leaseOwner,
1747
+ leaseToken: invocation.leaseToken,
1748
+ patch
1749
+ });
1750
+ if (!updated) throw new RecoverableFinalizationError(`Invocation lease lost: ${actionInvocationId}`);
1751
+ } else {
1752
+ await transaction.updateActionInvocation(actionInvocationId, tenantId, spaceId, patch);
1753
+ }
1754
+ return true;
1755
+ });
1756
+ let committed;
1757
+ try {
1758
+ committed = await finalize();
1759
+ } catch (error) {
1760
+ if (error instanceof RecoverableFinalizationError) throw error;
1761
+ try {
1762
+ committed = await finalize();
1763
+ } catch (retryError) {
1764
+ if (retryError instanceof RecoverableFinalizationError) throw retryError;
1765
+ return false;
1766
+ }
1767
+ }
1768
+ if (committed) emitInvocationStatusTelemetry(invocation, patch.status, options.telemetry, now(), previousStatus);
1769
+ return committed;
1770
+ }
1694
1771
  const adapterErrorContext = { actionId: action.actionId, source: "adapter", adapterType: step.adapterType, operation: step.operation, provider: adapter.vendor };
1695
1772
  const executionErrorContext = { actionId: action.actionId, source: "execution" };
1696
1773
  let errorContext = executionErrorContext;
@@ -1850,9 +1927,13 @@ function createGovernedActionHost(options) {
1850
1927
  ...deadlineMs2 ? { dueAt: new Date(acceptedAt.getTime() + deadlineMs2) } : {}
1851
1928
  } : void 0);
1852
1929
  try {
1853
- await persistReconciliationVerdict(invocation, { status: "reconciliation_required", error: message, ...pendingCompletion ? { pendingCompletion } : {} });
1854
- await appendDegradedAdapterEvidence(true, message, pendingCompletion?.adapterInvocationId === adapterInvocationId ? pendingCompletion : void 0, void 0, reference || void 0);
1855
- await options.store.updateAdapterInvocation(adapterInvocationId, { status: "succeeded", updatedAt: now() }, adapterInvocationScope);
1930
+ const patch = { status: "reconciliation_required", error: message, ...pendingCompletion ? { pendingCompletion } : {} };
1931
+ const accepted = pendingCompletion?.adapterInvocationId === adapterInvocationId ? pendingCompletion : void 0;
1932
+ if (!await finalizeDegradedAdapterAtomically(patch, true, message, accepted, void 0, reference || void 0)) {
1933
+ await persistReconciliationVerdict(invocation, patch);
1934
+ await appendDegradedAdapterEvidence(true, message, accepted, void 0, reference || void 0);
1935
+ await options.store.updateAdapterInvocation(adapterInvocationId, { status: "succeeded", updatedAt: now() }, adapterInvocationScope);
1936
+ }
1856
1937
  } catch {
1857
1938
  throw new RecoverableFinalizationError(message);
1858
1939
  }
@@ -1880,13 +1961,18 @@ function createGovernedActionHost(options) {
1880
1961
  const completion = action.execution?.completion ?? "immediate";
1881
1962
  const refuse = completion === "immediate" ? `Adapter ${step.adapterType}:${step.operation} handed off external operation "${reference}" but ${action.actionId} declares immediate completion; the external effect may be in flight.` : pendingHandoff ? `Adapter ${step.adapterType}:${step.operation} handed off a second external operation "${reference}" while "${pendingHandoff.externalReference}" is already pending; one invocation waits on one completion.` : void 0;
1882
1963
  if (refuse) {
1964
+ let adapterFinalizedAtomically;
1883
1965
  try {
1884
- await persistReconciliationVerdict(invocation, {
1966
+ const patch = {
1885
1967
  status: "reconciliation_required",
1886
1968
  error: refuse,
1887
1969
  ...pendingHandoff ? {} : { pendingCompletion: accepted }
1888
- });
1889
- await appendDegradedAdapterEvidence(false, refuse, pendingHandoff ? void 0 : accepted, output);
1970
+ };
1971
+ adapterFinalizedAtomically = await finalizeDegradedAdapterAtomically(patch, false, refuse, pendingHandoff ? void 0 : accepted, output);
1972
+ if (!adapterFinalizedAtomically) {
1973
+ await persistReconciliationVerdict(invocation, patch);
1974
+ await appendDegradedAdapterEvidence(false, refuse, pendingHandoff ? void 0 : accepted, output);
1975
+ }
1890
1976
  } catch {
1891
1977
  throw new RecoverableFinalizationError(refuse);
1892
1978
  }
@@ -1898,7 +1984,7 @@ function createGovernedActionHost(options) {
1898
1984
  await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, { error });
1899
1985
  }
1900
1986
  try {
1901
- await options.store.updateAdapterInvocation(adapterInvocationId, { status: "succeeded", output, updatedAt: now() }, adapterInvocationScope);
1987
+ if (!adapterFinalizedAtomically) await options.store.updateAdapterInvocation(adapterInvocationId, { status: "succeeded", output, updatedAt: now() }, adapterInvocationScope);
1902
1988
  } catch {
1903
1989
  throw new RecoverableFinalizationError(refuse);
1904
1990
  }
@@ -2672,11 +2758,14 @@ var MemoryPlatformHostStore = class {
2672
2758
  await previous;
2673
2759
  let domainSnapshot;
2674
2760
  let snapshotCreated = false;
2761
+ const undoLedgerWrites = [];
2675
2762
  try {
2676
2763
  domainSnapshot = transactionProvider.snapshot(this.db);
2677
2764
  snapshotCreated = true;
2678
2765
  const pendingEvents = [];
2679
2766
  const pendingUpdates = [];
2767
+ const pendingAdapterUpdates = [];
2768
+ const pendingReconciliations = [];
2680
2769
  const appendPending = async (event, metadata) => {
2681
2770
  if (this.events.some((candidate) => candidate.id === event.id) || pendingEvents.some((candidate) => candidate.event.id === event.id)) return;
2682
2771
  pendingEvents.push({ event, ...metadata ? { metadata } : {} });
@@ -2711,26 +2800,62 @@ var MemoryPlatformHostStore = class {
2711
2800
  }
2712
2801
  return state;
2713
2802
  },
2803
+ updateAdapterInvocation: async (id, patch, scope) => {
2804
+ if (!await this.getAdapterInvocation(id, scope)) throw new Error(`AdapterInvocation not found: ${id}`);
2805
+ pendingAdapterUpdates.push([id, patch, scope]);
2806
+ },
2807
+ appendExternalReconciliation: async (record) => {
2808
+ pendingReconciliations.push(record);
2809
+ },
2714
2810
  updateActionInvocation: async (id, tenantId, spaceId, patch) => {
2715
- pendingUpdates.push({ id, tenantId, spaceId, patch });
2811
+ pendingUpdates.push({ id, tenantId, spaceId, patch: copyInvocationPatch(patch) });
2716
2812
  },
2717
2813
  updateLeasedActionInvocation: async (input) => {
2718
2814
  const record = await this.getActionInvocation(input.id, input.tenantId, input.spaceId);
2719
2815
  if (!record || record.status !== "running" || record.leaseOwner !== input.workerId || record.leaseToken !== input.leaseToken) return false;
2720
- pendingUpdates.push({ id: input.id, tenantId: input.tenantId, spaceId: input.spaceId, patch: input.patch });
2816
+ pendingUpdates.push({ id: input.id, tenantId: input.tenantId, spaceId: input.spaceId, patch: copyInvocationPatch(input.patch) });
2721
2817
  return true;
2722
2818
  }
2723
2819
  });
2724
2820
  for (const update of pendingUpdates) {
2725
2821
  if (!await this.getActionInvocation(update.id, update.tenantId, update.spaceId)) throw new Error(`ActionInvocation not found: ${update.id}`);
2726
2822
  }
2823
+ const rememberRecord = (record) => {
2824
+ const before = { ...record };
2825
+ undoLedgerWrites.push(() => {
2826
+ for (const key of Object.keys(record)) Reflect.deleteProperty(record, key);
2827
+ Object.assign(record, before);
2828
+ });
2829
+ };
2830
+ const rememberAppend = (rows, id) => {
2831
+ if (!rows.some((row) => row.id === id)) undoLedgerWrites.push(() => {
2832
+ const index = rows.findIndex((row) => row.id === id);
2833
+ if (index !== -1) rows.splice(index, 1);
2834
+ });
2835
+ };
2727
2836
  for (const pending of pendingEvents) {
2837
+ rememberAppend(this.events, pending.event.id);
2838
+ rememberAppend(this.outbox, pending.event.id);
2728
2839
  if (pending.metadata) await this.appendEventWithOutbox(pending.event, pending.metadata);
2729
2840
  else await this.appendEvent(pending.event);
2730
2841
  }
2731
- for (const update of pendingUpdates) await this.updateActionInvocation(update.id, update.tenantId, update.spaceId, update.patch);
2842
+ for (const record of pendingReconciliations) {
2843
+ rememberAppend(this.externalReconciliations, record.id);
2844
+ await this.appendExternalReconciliation(record);
2845
+ }
2846
+ for (const [id, patch, scope] of pendingAdapterUpdates) {
2847
+ const record = await this.getAdapterInvocation(id, scope);
2848
+ if (!record) throw new Error(`AdapterInvocation not found: ${id}`);
2849
+ rememberRecord(record);
2850
+ await this.updateAdapterInvocation(id, patch, scope);
2851
+ }
2852
+ for (const update of pendingUpdates) {
2853
+ rememberRecord(await this.getActionInvocation(update.id, update.tenantId, update.spaceId));
2854
+ await this.updateActionInvocation(update.id, update.tenantId, update.spaceId, update.patch);
2855
+ }
2732
2856
  return result;
2733
2857
  } catch (error) {
2858
+ for (const undo of undoLedgerWrites.reverse()) undo();
2734
2859
  if (snapshotCreated) transactionProvider.restore(this.db, domainSnapshot);
2735
2860
  throw error;
2736
2861
  } finally {
@@ -2758,6 +2883,10 @@ var MemoryPlatformHostStore = class {
2758
2883
  const record = await this.getActionInvocation(id, tenantId, spaceId);
2759
2884
  if (!record) throw new Error(`ActionInvocation not found: ${id}`);
2760
2885
  Object.assign(record, patch, { updatedAt: /* @__PURE__ */ new Date() });
2886
+ if (Object.hasOwn(patch, "pendingStateTransition")) {
2887
+ if (patch.pendingStateTransition) record.pendingStateTransition = { ...patch.pendingStateTransition };
2888
+ else delete record.pendingStateTransition;
2889
+ }
2761
2890
  if (Object.hasOwn(patch, "pendingCompletion") && patch.pendingCompletion === void 0) delete record.pendingCompletion;
2762
2891
  if (Object.hasOwn(patch, "externalCompletion") && patch.externalCompletion === void 0) delete record.externalCompletion;
2763
2892
  if (Object.hasOwn(patch, "error") && patch.error === void 0) delete record.error;
@@ -3045,6 +3174,9 @@ function cloneOutboxRequeue(requeue) {
3045
3174
  priorAvailableAt: new Date(requeue.priorAvailableAt)
3046
3175
  };
3047
3176
  }
3177
+ function copyInvocationPatch(patch) {
3178
+ return { ...patch, ...patch.pendingStateTransition ? { pendingStateTransition: { ...patch.pendingStateTransition } } : {} };
3179
+ }
3048
3180
  async function applyPostgresMigrations(client, migrations) {
3049
3181
  const ordered = validateMigrations(migrations);
3050
3182
  const statements = ordered.map((migration) => migrationStatement(migration));
@@ -3141,7 +3273,9 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
3141
3273
  getEntityStateSource: (tenantId, spaceId, entityType, entityId) => scoped.getEntityStateSource(tenantId, spaceId, entityType, entityId),
3142
3274
  getEntityState: (tenantId, spaceId, entityType, entityId) => scoped.getEntityState(tenantId, spaceId, entityType, entityId),
3143
3275
  updateActionInvocation: (id, tenantId, spaceId, patch) => scoped.updateActionInvocation(id, tenantId, spaceId, patch),
3144
- updateLeasedActionInvocation: (input) => scoped.updateLeasedActionInvocation(input)
3276
+ updateLeasedActionInvocation: (input) => scoped.updateLeasedActionInvocation(input),
3277
+ updateAdapterInvocation: (id, patch, scope) => scoped.updateAdapterInvocation(id, patch, scope),
3278
+ appendExternalReconciliation: (record) => scoped.appendExternalReconciliation(record)
3145
3279
  });
3146
3280
  });
3147
3281
  }
@@ -3324,6 +3458,13 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
3324
3458
  ALTER TABLE fabric_platform.action_invocations
3325
3459
  ADD COLUMN admitted_at timestamptz;
3326
3460
  `
3461
+ }, {
3462
+ version: 7,
3463
+ name: "pending_state_transition",
3464
+ sql: `
3465
+ ALTER TABLE fabric_platform.action_invocations
3466
+ ADD COLUMN IF NOT EXISTS pending_state_transition jsonb;
3467
+ `
3327
3468
  }]);
3328
3469
  }
3329
3470
  async transaction(run) {
@@ -3404,6 +3545,7 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
3404
3545
  pending_completion=CASE WHEN $12::boolean THEN $13::jsonb ELSE pending_completion END,
3405
3546
  completion_due_at=CASE WHEN $12::boolean THEN ($13::jsonb->>'dueAt')::timestamptz ELSE completion_due_at END,
3406
3547
  external_completion=CASE WHEN $14::boolean THEN $15::jsonb ELSE external_completion END,
3548
+ pending_state_transition=CASE WHEN $16::boolean THEN $17::jsonb ELSE pending_state_transition END,
3407
3549
  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')
3408
3550
  THEN NULL ELSE lease_owner END,
3409
3551
  lease_expires_at=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')
@@ -3425,7 +3567,9 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
3425
3567
  Object.hasOwn(patch, "pendingCompletion"),
3426
3568
  patch.pendingCompletion ? JSON.stringify(patch.pendingCompletion) : null,
3427
3569
  Object.hasOwn(patch, "externalCompletion"),
3428
- patch.externalCompletion ? JSON.stringify(patch.externalCompletion) : null
3570
+ patch.externalCompletion ? JSON.stringify(patch.externalCompletion) : null,
3571
+ Object.hasOwn(patch, "pendingStateTransition"),
3572
+ patch.pendingStateTransition ? JSON.stringify(patch.pendingStateTransition) : null
3429
3573
  ]
3430
3574
  );
3431
3575
  }
@@ -3977,6 +4121,7 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
3977
4121
  pending_completion=CASE WHEN $14::boolean THEN $15::jsonb ELSE pending_completion END,
3978
4122
  completion_due_at=CASE WHEN $14::boolean THEN ($15::jsonb->>'dueAt')::timestamptz ELSE completion_due_at END,
3979
4123
  external_completion=CASE WHEN $16::boolean THEN $17::jsonb ELSE external_completion END,
4124
+ pending_state_transition=CASE WHEN $18::boolean THEN $19::jsonb ELSE pending_state_transition END,
3980
4125
  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')
3981
4126
  THEN NULL ELSE lease_owner END,
3982
4127
  lease_expires_at=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')
@@ -4002,7 +4147,9 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
4002
4147
  Object.hasOwn(patch, "pendingCompletion"),
4003
4148
  patch.pendingCompletion ? JSON.stringify(patch.pendingCompletion) : null,
4004
4149
  Object.hasOwn(patch, "externalCompletion"),
4005
- patch.externalCompletion ? JSON.stringify(patch.externalCompletion) : null
4150
+ patch.externalCompletion ? JSON.stringify(patch.externalCompletion) : null,
4151
+ Object.hasOwn(patch, "pendingStateTransition"),
4152
+ patch.pendingStateTransition ? JSON.stringify(patch.pendingStateTransition) : null
4006
4153
  ]
4007
4154
  );
4008
4155
  return result.rows.length === 1;
@@ -4101,6 +4248,7 @@ function toActionRecord(row) {
4101
4248
  ...row.execution_reason ? { executionReason: String(row.execution_reason) } : {},
4102
4249
  ...row.authorization_reconciliation ? { authorizationReconciliation: row.authorization_reconciliation } : {},
4103
4250
  ...row.adapter_reconciliation ? { adapterReconciliation: row.adapter_reconciliation } : {},
4251
+ ...row.pending_state_transition ? { pendingStateTransition: row.pending_state_transition } : {},
4104
4252
  ...row.pending_completion ? { pendingCompletion: toPendingCompletion(row.pending_completion) } : {},
4105
4253
  ...row.external_completion ? { externalCompletion: toExternalCompletion(row.external_completion) } : {},
4106
4254
  ...row.authorization_binding_id ? { authorizationBindingId: String(row.authorization_binding_id) } : {},