@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/CHANGELOG.md +24 -0
- package/README.md +101 -20
- package/dist/index.cjs +167 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -2
- package/dist/index.d.ts +27 -2
- package/dist/index.js +168 -20
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ActorType, ActionId, ActionStatus, EvidencePacketReference, AgentActionRoute, AgentActionRiskTier, MutationFootprint, ExecutionPrincipal, GovernanceRuntimeEvidence, PolicyOutcome, AssetEventEnvelope,
|
|
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,
|
|
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?.
|
|
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
|
-
|
|
1854
|
-
|
|
1855
|
-
await
|
|
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
|
-
|
|
1966
|
+
const patch = {
|
|
1885
1967
|
status: "reconciliation_required",
|
|
1886
1968
|
error: refuse,
|
|
1887
1969
|
...pendingHandoff ? {} : { pendingCompletion: accepted }
|
|
1888
|
-
}
|
|
1889
|
-
await
|
|
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
|
|
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) } : {},
|