@fabricorg/platform-host 8.4.0 → 8.5.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,8 +1,102 @@
1
+ import { types } from "util";
1
2
  import { AdapterCircuitBreakerRegistry, AdapterRegistry, FABRIC_GOVERNANCE_CONTRACT_VERSION, ReconciliationRequiredError, aggregatePolicyOutcomes, assertActionFinalization, assertAdapterCircuitBreakerConfig, assertAdapterOutcomeConsistency, assertAdapterStepDeadlines, assertGovernanceRuntimeEvidence, assertMutationGovernanceContext, canonicalJson, createFabricId, evaluatePolicyDefinitions, evaluateStateMachineTransition, executeWithAdapterRetry, isAdapterCircuitOpenError, isReconciliationRequiredError, isRetryableExecutionError } from "@fabricorg/platform";
2
3
  import { performance } from "perf_hooks";
3
4
  import { assertAssemblyRuntimeCompatible } from "@fabricorg/assembly";
4
5
  import { createHash } from "crypto";
5
- import { types } from "util";
6
+ //#region src/action-finalization.ts
7
+ /** Remove private preparation and restore its compatible protocol on settlement. */
8
+ function finalizationSettlementPatch(record, patch) {
9
+ const checkpoint = Object.hasOwn(patch, "finalizationCheckpoint") ? patch.finalizationCheckpoint : record.finalizationCheckpoint;
10
+ if (!checkpoint || !patch.status || ![
11
+ "completed",
12
+ "failed",
13
+ "blocked_by_policy",
14
+ "validation_failed"
15
+ ].includes(patch.status) || !Object.hasOwn(checkpoint, "adapterPreparation") && checkpoint.protocol !== "atomic-after-adapters-v2") return patch;
16
+ const { adapterPreparation: _preparation, ...evidence } = checkpoint;
17
+ return {
18
+ ...patch,
19
+ finalizationCheckpoint: {
20
+ ...evidence,
21
+ protocol: "atomic-after-adapters-v1"
22
+ }
23
+ };
24
+ }
25
+ /** Strict JSON evidence: reject lossy serialization rather than silently dropping values. */
26
+ function finalizationJson(value) {
27
+ const ancestors = /* @__PURE__ */ new Set();
28
+ function check(item) {
29
+ if (item === null || typeof item === "string" || typeof item === "boolean") return;
30
+ if (typeof item === "number" && Number.isFinite(item)) return;
31
+ if (typeof item !== "object" || !item || ancestors.has(item)) throw new Error("Finalization requires JSON-compatible data.");
32
+ if (!Array.isArray(item) && Object.getPrototypeOf(item) !== Object.prototype && Object.getPrototypeOf(item) !== null) throw new Error("Finalization requires plain JSON objects.");
33
+ if (Object.getOwnPropertySymbols(item).length > 0) throw new Error("Finalization requires JSON string keys.");
34
+ if (Array.isArray(item)) {
35
+ const keys = Object.keys(item);
36
+ if (keys.length !== item.length || keys.some((key, index) => key !== String(index))) throw new Error("Finalization requires dense JSON arrays.");
37
+ }
38
+ ancestors.add(item);
39
+ for (const child of Object.values(item)) check(child);
40
+ ancestors.delete(item);
41
+ }
42
+ check(value);
43
+ return JSON.parse(JSON.stringify(value));
44
+ }
45
+ /** Match the persisted JSON representation before validating finalization execution input. */
46
+ function normalizeFinalizationJson(value) {
47
+ return finalizationJson(JSON.parse(JSON.stringify(value)));
48
+ }
49
+ /** Omit only public optional fields; payloads and patch values retain strict JSON validation. */
50
+ function omitUndefinedOptionalFields(value, keys) {
51
+ if (!value || typeof value !== "object" || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) return value;
52
+ const copy = { ...value };
53
+ for (const key of keys) if (Object.hasOwn(copy, key) && copy[key] === void 0) delete copy[key];
54
+ return copy;
55
+ }
56
+ function validateFinalization(value, action, privateFields, contracts) {
57
+ const normalized = omitUndefinedOptionalFields(value, [
58
+ "events",
59
+ "resultPatch",
60
+ "stagedInvocations"
61
+ ]);
62
+ if (normalized !== value && normalized && typeof normalized === "object" && !Array.isArray(normalized)) {
63
+ const record = normalized;
64
+ if (Array.isArray(record.events)) {
65
+ const events = Object.assign(record.events.slice(), record.events);
66
+ for (let index = 0; index < events.length; index++) if (Object.hasOwn(events, index)) events[index] = omitUndefinedOptionalFields(events[index], ["eventSchemaVersion"]);
67
+ record.events = events;
68
+ }
69
+ }
70
+ const result = finalizationJson(normalized);
71
+ if (!result || typeof result !== "object" || Array.isArray(result) || Object.keys(result).some((key) => key !== "events" && key !== "resultPatch" && key !== "stagedInvocations")) throw new Error("Finalization must return only events, resultPatch and stagedInvocations.");
72
+ if (result.resultPatch !== void 0) {
73
+ if (!result.resultPatch || typeof result.resultPatch !== "object" || Array.isArray(result.resultPatch)) throw new Error("Finalization resultPatch must be an object.");
74
+ if (Object.keys(result.resultPatch).some((key) => privateFields.includes(key))) throw new Error("Finalization resultPatch contains a private result key.");
75
+ }
76
+ if (result.events !== void 0) {
77
+ if (!Array.isArray(result.events)) throw new Error("Finalization events must be an array.");
78
+ for (const event of result.events) {
79
+ if (!event || typeof event !== "object" || Array.isArray(event) || Object.keys(event).some((key) => ![
80
+ "eventType",
81
+ "subjectType",
82
+ "subjectId",
83
+ "payload",
84
+ "eventSchemaVersion"
85
+ ].includes(key))) throw new Error("Invalid finalization event fields.");
86
+ for (const key of [
87
+ "eventType",
88
+ "subjectType",
89
+ "subjectId"
90
+ ]) if (typeof event[key] !== "string" || !event[key].trim()) throw new Error("Finalization event identifiers must be non-empty.");
91
+ if (!Object.hasOwn(event, "payload")) throw new Error("Finalization event requires a JSON payload.");
92
+ if (event.eventSchemaVersion !== void 0 && (!Number.isInteger(event.eventSchemaVersion) || event.eventSchemaVersion <= 0)) throw new Error("Finalization eventSchemaVersion must be a positive integer.");
93
+ if (!action.emitsEvents.includes(event.eventType)) throw new Error("Finalization emitted undeclared event type.");
94
+ if (contracts && (!contracts.hasEventType(event.eventType) || !contracts.hasSubjectType(event.subjectType))) throw new Error("Finalization emitted an unregistered event or subject type.");
95
+ }
96
+ }
97
+ return result;
98
+ }
99
+ //#endregion
6
100
  //#region src/types.ts
7
101
  /** Durable Host lifecycle generation, independent from the npm package version. */
8
102
  const PLATFORM_HOST_CONTRACT_VERSION = 2;
@@ -68,6 +162,139 @@ var ExternalCompletionError = class extends Error {
68
162
  return this.refusal === "still_executing";
69
163
  }
70
164
  };
165
+ /** Maximum serialized UTF-8 evidence size. */
166
+ const MAX_APPROVAL_EVIDENCE_BYTES = 16384;
167
+ const APPROVAL_REQUESTED_EVENT = "ApprovalRequested";
168
+ const APPROVAL_DECIDED_EVENT = "ApprovalDecided";
169
+ const APPROVAL_EXPIRED_EVENT = "ApprovalExpired";
170
+ //#endregion
171
+ //#region src/approval-lifecycle.ts
172
+ function validApprovalDate(value) {
173
+ return value instanceof Date && Number.isFinite(value.getTime());
174
+ }
175
+ /** Snapshot before awaiting application authorization; reject lossy JSON conversions. */
176
+ function snapshotApprovalDecision(decision) {
177
+ const visit = (value, ancestors, depth) => {
178
+ if (value === null || typeof value === "string" || typeof value === "boolean") return;
179
+ if (typeof value === "number" && Number.isFinite(value)) return;
180
+ if (typeof value !== "object" || types.isProxy(value)) throw new Error("Approval evidence must be plain JSON data.");
181
+ if (ancestors.includes(value)) throw new Error("Approval evidence must not contain cycles.");
182
+ if (depth > 64) throw new Error("Approval evidence exceeds maximum depth 64.");
183
+ const array = Array.isArray(value);
184
+ if (!array && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new Error("Approval evidence must contain only plain objects and JSON values.");
185
+ const keys = Reflect.ownKeys(value);
186
+ if (array && keys.length !== value.length + 1) throw new Error("Approval evidence arrays must be dense JSON arrays.");
187
+ for (const key of keys) {
188
+ if (array && key === "length") continue;
189
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
190
+ if (typeof key !== "string" || !descriptor.enumerable || !Object.hasOwn(descriptor, "value") || array && !/^(0|[1-9][0-9]*)$/.test(key)) throw new Error("Approval evidence must contain enumerable JSON data properties only.");
191
+ visit(descriptor.value, [...ancestors, value], depth + 1);
192
+ }
193
+ };
194
+ if (types.isProxy(decision)) throw new Error("Approval decision must be plain data.");
195
+ for (const descriptor of Object.values(Object.getOwnPropertyDescriptors(decision))) if (!Object.hasOwn(descriptor, "value")) throw new Error("Approval decision must be plain data.");
196
+ if (decision.evidence !== void 0) {
197
+ if (!decision.evidence || typeof decision.evidence !== "object" || Array.isArray(decision.evidence)) throw new Error("Approval evidence must be a JSON object.");
198
+ visit(decision.evidence, [], 0);
199
+ if (Buffer.byteLength(JSON.stringify(decision.evidence), "utf8") > 16384) throw new Error(`Approval evidence exceeds ${MAX_APPROVAL_EVIDENCE_BYTES} bytes.`);
200
+ }
201
+ if (decision.decidedAt !== void 0 && !validApprovalDate(decision.decidedAt)) throw new Error("decidedAt must be a valid Date.");
202
+ return structuredClone(decision);
203
+ }
204
+ function approvalSweepLimit(input) {
205
+ if (!validApprovalDate(input.now)) throw new Error("Approval sweep now must be a valid Date.");
206
+ const limit = input.limit ?? 100;
207
+ if (!Number.isInteger(limit) || limit < 1 || limit > 1e3) throw new Error("Approval sweep limit must be an integer from 1 to 1000.");
208
+ return limit;
209
+ }
210
+ function approvalExpiredPatch(record, now) {
211
+ return finalizationSettlementPatch(record, {
212
+ status: "failed",
213
+ approvalDecision: void 0,
214
+ approvalExpiry: {
215
+ expiresAt: new Date(record.approvalExpiresAt),
216
+ expiredAt: new Date(now)
217
+ },
218
+ error: "Approval expired before a decision was applied."
219
+ });
220
+ }
221
+ /** Both stores serialize this read, event append, and update within their mutation transaction. */
222
+ async function applyApprovalTransition(transaction, input, kind) {
223
+ if (!transaction.getActionInvocationForUpdate) throw new Error("Approval transitions require locking invocation reads.");
224
+ const record = await transaction.getActionInvocationForUpdate(input.actionInvocationId, input.tenantId, input.spaceId);
225
+ if (!record) return { applied: false };
226
+ if (kind === "decided" && transaction.beginApprovalDecision) {
227
+ const { lifecycleEvent, ...decisionInput } = input;
228
+ const result = await transaction.beginApprovalDecision(decisionInput);
229
+ if (result.applied && result.invocation) await appendLifecycleEvent(transaction, lifecycleEvent, result.invocation, result.invocation.approvalExpiry ? "expired" : "decided");
230
+ return result;
231
+ }
232
+ let patch;
233
+ if (kind === "requested") {
234
+ if (!["pending", "running"].includes(record.status) || record.approvalDecision !== void 0 || record.leaseOwner !== void 0 && (record.leaseOwner !== input.leaseOwner || record.leaseToken !== input.leaseToken)) return {
235
+ applied: false,
236
+ invocation: record
237
+ };
238
+ patch = {
239
+ status: "waiting_for_approval",
240
+ ...Object.hasOwn(input, "approvalExpiresAt") ? { approvalExpiresAt: input.approvalExpiresAt } : {}
241
+ };
242
+ } else {
243
+ if (record.status !== "waiting_for_approval") return {
244
+ applied: false,
245
+ invocation: record
246
+ };
247
+ const due = record.approvalExpiresAt !== void 0 && record.approvalExpiresAt.getTime() <= input.now.getTime();
248
+ if (kind === "expired" && !due) return {
249
+ applied: false,
250
+ invocation: record
251
+ };
252
+ if (due) {
253
+ kind = "expired";
254
+ patch = approvalExpiredPatch(record, input.now);
255
+ } else {
256
+ const decisionInput = input;
257
+ patch = decisionInput.decision.approved ? {
258
+ status: "running",
259
+ error: void 0,
260
+ approvalDecision: structuredClone(decisionInput.decision),
261
+ leaseOwner: decisionInput.leaseOwner,
262
+ leaseToken: (record.leaseToken ?? 0) + 1,
263
+ leaseExpiresAt: new Date(input.now.getTime() + decisionInput.leaseDurationMs),
264
+ attemptCount: Math.max(record.attemptCount, 1)
265
+ } : finalizationSettlementPatch(record, {
266
+ status: "failed",
267
+ approvalDecision: structuredClone(decisionInput.decision),
268
+ error: `Approval rejected: ${decisionInput.decision.reason ?? "no reason provided"}`
269
+ });
270
+ }
271
+ }
272
+ const transitioned = {
273
+ ...record,
274
+ ...patch,
275
+ updatedAt: new Date(input.now)
276
+ };
277
+ if (Object.hasOwn(patch, "approvalDecision") && patch.approvalDecision === void 0) delete transitioned.approvalDecision;
278
+ if (transitioned.status !== "running") {
279
+ delete transitioned.leaseOwner;
280
+ delete transitioned.leaseExpiresAt;
281
+ }
282
+ await appendLifecycleEvent(transaction, input.lifecycleEvent, transitioned, kind);
283
+ await transaction.updateActionInvocation(record.id, record.tenantId, record.spaceId, patch);
284
+ return {
285
+ applied: true,
286
+ invocation: transitioned
287
+ };
288
+ }
289
+ async function appendLifecycleEvent(transaction, factory, invocation, kind) {
290
+ if (!factory) return;
291
+ if (!transaction.appendEventWithOutbox) throw new Error("Approval events require atomic outbox writes.");
292
+ const { event, metadata } = factory(invocation, kind);
293
+ await transaction.appendEventWithOutbox({
294
+ ...event,
295
+ sequence: await transaction.nextEventSequence(invocation.tenantId, invocation.spaceId)
296
+ }, metadata);
297
+ }
71
298
  //#endregion
72
299
  //#region src/command-identity.ts
73
300
  const PARAMETER_DIGEST_ALGORITHM = "fabric-canonical-json-sha256-v1";
@@ -107,82 +334,6 @@ function normalizeProvenance(input, options) {
107
334
  };
108
335
  }
109
336
  //#endregion
110
- //#region src/action-finalization.ts
111
- /** Strict JSON evidence: reject lossy serialization rather than silently dropping values. */
112
- function finalizationJson(value) {
113
- const ancestors = /* @__PURE__ */ new Set();
114
- function check(item) {
115
- if (item === null || typeof item === "string" || typeof item === "boolean") return;
116
- if (typeof item === "number" && Number.isFinite(item)) return;
117
- if (typeof item !== "object" || !item || ancestors.has(item)) throw new Error("Finalization requires JSON-compatible data.");
118
- if (!Array.isArray(item) && Object.getPrototypeOf(item) !== Object.prototype && Object.getPrototypeOf(item) !== null) throw new Error("Finalization requires plain JSON objects.");
119
- if (Object.getOwnPropertySymbols(item).length > 0) throw new Error("Finalization requires JSON string keys.");
120
- if (Array.isArray(item)) {
121
- const keys = Object.keys(item);
122
- if (keys.length !== item.length || keys.some((key, index) => key !== String(index))) throw new Error("Finalization requires dense JSON arrays.");
123
- }
124
- ancestors.add(item);
125
- for (const child of Object.values(item)) check(child);
126
- ancestors.delete(item);
127
- }
128
- check(value);
129
- return JSON.parse(JSON.stringify(value));
130
- }
131
- /** Match the persisted JSON representation before validating finalization execution input. */
132
- function normalizeFinalizationJson(value) {
133
- return finalizationJson(JSON.parse(JSON.stringify(value)));
134
- }
135
- /** Omit only public optional fields; payloads and patch values retain strict JSON validation. */
136
- function omitUndefinedOptionalFields(value, keys) {
137
- if (!value || typeof value !== "object" || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) return value;
138
- const copy = { ...value };
139
- for (const key of keys) if (Object.hasOwn(copy, key) && copy[key] === void 0) delete copy[key];
140
- return copy;
141
- }
142
- function validateFinalization(value, action, privateFields, contracts) {
143
- const normalized = omitUndefinedOptionalFields(value, [
144
- "events",
145
- "resultPatch",
146
- "stagedInvocations"
147
- ]);
148
- if (normalized !== value && normalized && typeof normalized === "object" && !Array.isArray(normalized)) {
149
- const record = normalized;
150
- if (Array.isArray(record.events)) {
151
- const events = Object.assign(record.events.slice(), record.events);
152
- for (let index = 0; index < events.length; index++) if (Object.hasOwn(events, index)) events[index] = omitUndefinedOptionalFields(events[index], ["eventSchemaVersion"]);
153
- record.events = events;
154
- }
155
- }
156
- const result = finalizationJson(normalized);
157
- if (!result || typeof result !== "object" || Array.isArray(result) || Object.keys(result).some((key) => key !== "events" && key !== "resultPatch" && key !== "stagedInvocations")) throw new Error("Finalization must return only events, resultPatch and stagedInvocations.");
158
- if (result.resultPatch !== void 0) {
159
- if (!result.resultPatch || typeof result.resultPatch !== "object" || Array.isArray(result.resultPatch)) throw new Error("Finalization resultPatch must be an object.");
160
- if (Object.keys(result.resultPatch).some((key) => privateFields.includes(key))) throw new Error("Finalization resultPatch contains a private result key.");
161
- }
162
- if (result.events !== void 0) {
163
- if (!Array.isArray(result.events)) throw new Error("Finalization events must be an array.");
164
- for (const event of result.events) {
165
- if (!event || typeof event !== "object" || Array.isArray(event) || Object.keys(event).some((key) => ![
166
- "eventType",
167
- "subjectType",
168
- "subjectId",
169
- "payload",
170
- "eventSchemaVersion"
171
- ].includes(key))) throw new Error("Invalid finalization event fields.");
172
- for (const key of [
173
- "eventType",
174
- "subjectType",
175
- "subjectId"
176
- ]) if (typeof event[key] !== "string" || !event[key].trim()) throw new Error("Finalization event identifiers must be non-empty.");
177
- if (!Object.hasOwn(event, "payload")) throw new Error("Finalization event requires a JSON payload.");
178
- if (event.eventSchemaVersion !== void 0 && (!Number.isInteger(event.eventSchemaVersion) || event.eventSchemaVersion <= 0)) throw new Error("Finalization eventSchemaVersion must be a positive integer.");
179
- if (!action.emitsEvents.includes(event.eventType)) throw new Error("Finalization emitted undeclared event type.");
180
- if (contracts && (!contracts.hasEventType(event.eventType) || !contracts.hasSubjectType(event.subjectType))) throw new Error("Finalization emitted an unregistered event or subject type.");
181
- }
182
- }
183
- return result;
184
- }
185
- //#endregion
186
337
  //#region src/observability.ts
187
338
  /** Version of the vendor-neutral health/readiness contract. */
188
339
  const PLATFORM_HOST_HEALTH_CONTRACT_VERSION = 1;
@@ -589,7 +740,7 @@ function createExternalCompletionLifecycle(deps) {
589
740
  externalCompletion,
590
741
  pendingCompletion: void 0
591
742
  };
592
- await writer.update(patch);
743
+ await writer.update(finalizationSettlementPatch(invocation, patch));
593
744
  if (settles) emitInvocationStatusTelemetry(invocation, "completed", deps.telemetry, now(), invocation.status);
594
745
  return withConsistency(actionResult({
595
746
  ...invocation,
@@ -626,7 +777,7 @@ function createExternalCompletionLifecycle(deps) {
626
777
  externalCompletion,
627
778
  pendingCompletion: void 0
628
779
  };
629
- await writer.update(patch);
780
+ await writer.update(finalizationSettlementPatch(invocation, patch));
630
781
  if (settles) emitInvocationStatusTelemetry(invocation, "failed", deps.telemetry, now(), invocation.status);
631
782
  return withConsistency(actionResult({
632
783
  ...invocation,
@@ -1322,6 +1473,11 @@ const STAGED_ERROR = Object.freeze({
1322
1473
  lookup: "Staged child lookup failed."
1323
1474
  });
1324
1475
  function createGovernedActionHost(options) {
1476
+ if (options.approvalDefaultTtlMs !== void 0 && (!Number.isSafeInteger(options.approvalDefaultTtlMs) || options.approvalDefaultTtlMs <= 0)) throw new Error("approvalDefaultTtlMs must be a positive safe integer in milliseconds.");
1477
+ if (options.approvalLifecycleEvents) {
1478
+ const approvalStore = asApprovalStore(options.store);
1479
+ if (!approvalStore || approvalStore.transactionalApprovalLifecycle !== true || typeof approvalStore.parkApproval !== "function" || typeof approvalStore.expireDueApprovals !== "function" || asOutboxStore(options.store)?.transactionalOutbox !== true || !asAtomicMutationStore(options.store)) throw new Error("Approval lifecycle events require transactionalOutbox, transactionWithEvents, and atomic approval lifecycle capabilities.");
1480
+ }
1325
1481
  const retryableExecutionMaxAttempts = options.retryableExecutionMaxAttempts ?? 5;
1326
1482
  if (!Number.isInteger(retryableExecutionMaxAttempts) || retryableExecutionMaxAttempts <= 0) throw new Error("retryableExecutionMaxAttempts must be a positive integer.");
1327
1483
  const recoveryMaxAttempts = options.recoveryMaxAttempts ?? Math.max(5, retryableExecutionMaxAttempts);
@@ -1697,6 +1853,7 @@ function createGovernedActionHost(options) {
1697
1853
  };
1698
1854
  }
1699
1855
  async function persistExecutionInvocation(record, patch) {
1856
+ patch = finalizationSettlementPatch(record, patch);
1700
1857
  const externalCompletionDigest = record.externalCompletion?.digest;
1701
1858
  if (await persistInvocation(record, patch, onPreservedReconciliation, executionLease)) rememberVerdict(patch, externalCompletionDigest);
1702
1859
  }
@@ -2109,7 +2266,7 @@ function createGovernedActionHost(options) {
2109
2266
  }
2110
2267
  const replayCheckpoint = action.finalize && (resumingRunningInvocation || invocation.attemptCount > 1) ? invocation.finalizationCheckpoint : void 0;
2111
2268
  if (replayCheckpoint) {
2112
- 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.");
2269
+ if (!["atomic-after-adapters-v1", "atomic-after-adapters-v2"].includes(replayCheckpoint.protocol) || replayCheckpoint.protocol === "atomic-after-adapters-v2" !== Object.hasOwn(replayCheckpoint, "adapterPreparation") || 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.");
2113
2270
  }
2114
2271
  if (action.finalize && !replayCheckpoint && (resumingRunningInvocation || invocation.attemptCount > 1) && hasDurableProviderEffect) return interruptedNonIdempotent("Finalizing action recovery requires a preparation checkpoint; reconciliation is required.");
2115
2272
  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.`));
@@ -2180,6 +2337,7 @@ function createGovernedActionHost(options) {
2180
2337
  const hitlParameters = isRecord(parsed.data) ? parsed.data : invocation.parameters;
2181
2338
  const approvalStore = asApprovalStore(options.store);
2182
2339
  if (!approvalStore) return hostOperation(() => failExecution(invocation, "failed", "The configured HITL evaluator requires a store with approval persistence support."));
2340
+ let evaluatedApprovalExpiresAt;
2183
2341
  if (!invocation.hitlRoute) {
2184
2342
  const decision = await options.hitlEvaluator({
2185
2343
  actionId: action.actionId,
@@ -2193,6 +2351,8 @@ function createGovernedActionHost(options) {
2193
2351
  ...stringValue(hitlParameters.agentSessionId) ? { agentSessionId: stringValue(hitlParameters.agentSessionId) } : {},
2194
2352
  ...stringValue(hitlParameters.agentRunId) ? { agentRunId: stringValue(hitlParameters.agentRunId) } : {}
2195
2353
  });
2354
+ if (decision.approvalExpiresAt !== void 0 && !validApprovalDate(decision.approvalExpiresAt)) return hostOperation(() => failExecution(invocation, "failed", "approvalExpiresAt must be a valid Date."));
2355
+ evaluatedApprovalExpiresAt = decision.approvalExpiresAt === void 0 ? void 0 : new Date(decision.approvalExpiresAt);
2196
2356
  invocation = { ...await hostOperation(() => approvalStore.recordHitlDecision(actionInvocationId, tenantId, spaceId, {
2197
2357
  ...decision,
2198
2358
  ...options.hitlPolicyVersion ? { policyVersion: options.hitlPolicyVersion } : {},
@@ -2201,11 +2361,33 @@ function createGovernedActionHost(options) {
2201
2361
  }
2202
2362
  if (invocation.hitlRoute === "rejected") return hostOperation(() => failExecution(invocation, "failed", `HITL rejected: ${invocation.hitlReason ?? "no reason provided"}`));
2203
2363
  if ((invocation.hitlRoute === "needs-approval" || invocation.hitlRoute === "escalate") && !invocation.approvalDecision?.approved) {
2204
- await hostOperation(() => persistExecutionInvocation(invocation, { status: "waiting_for_approval" }));
2205
- return {
2206
- ...actionResult(invocation),
2207
- status: "waiting_for_approval"
2208
- };
2364
+ const parkedAt = now();
2365
+ const approvalExpiresAt = invocation.approvalExpiresAt ?? evaluatedApprovalExpiresAt ?? (options.approvalDefaultTtlMs !== void 0 ? new Date(parkedAt.getTime() + options.approvalDefaultTtlMs) : void 0);
2366
+ if (evaluatedApprovalExpiresAt !== void 0 && invocation.approvalExpiresAt === void 0) return hostOperation(() => failExecution(invocation, "failed", "The configured HITL evaluator requires a store with approval deadline persistence support."));
2367
+ if (approvalExpiresAt && !validApprovalDate(approvalExpiresAt)) return hostOperation(() => failExecution(invocation, "failed", "Approval deadline exceeds the supported Date range."));
2368
+ if (approvalExpiresAt && (!approvalStore.expireDueApprovals || !approvalStore.parkApproval)) return hostOperation(() => failExecution(invocation, "failed", "Approval deadlines require a store with approval expiry support."));
2369
+ if (approvalStore.parkApproval) {
2370
+ let transition;
2371
+ try {
2372
+ transition = await approvalStore.parkApproval({
2373
+ actionInvocationId,
2374
+ tenantId,
2375
+ spaceId,
2376
+ now: parkedAt,
2377
+ ...approvalExpiresAt ? { approvalExpiresAt } : {},
2378
+ ...executionLease,
2379
+ ...options.approvalLifecycleEvents ? { lifecycleEvent: approvalLifecycleEvent } : {}
2380
+ });
2381
+ } catch (error) {
2382
+ throw new RecoverableFinalizationError(caughtError({
2383
+ actionId: invocation.actionId,
2384
+ source: "execution"
2385
+ }, error));
2386
+ }
2387
+ invocation = transition.invocation ?? await options.store.getActionInvocation(actionInvocationId, tenantId, spaceId) ?? invocation;
2388
+ if (transition.applied) emitInvocationStatusTelemetry(invocation, "waiting_for_approval", options.telemetry, parkedAt);
2389
+ } else await hostOperation(() => persistExecutionInvocation(invocation, { status: "waiting_for_approval" }));
2390
+ return actionResult(invocation);
2209
2391
  }
2210
2392
  }
2211
2393
  const authorizationInput = toAuthorizationInput(action, invocation);
@@ -2438,13 +2620,23 @@ function createGovernedActionHost(options) {
2438
2620
  ...isAdmissionTime(invocation.admittedAt) ? { admittedAt: invocation.admittedAt } : {},
2439
2621
  db,
2440
2622
  services: options.services,
2441
- ...action.finalize ? { finalizationProtocol: "atomic-after-adapters-v1" } : {}
2623
+ ...action.finalize ? {
2624
+ finalizationProtocol: "atomic-after-adapters-v1",
2625
+ adapterPreparationProtocol: "v1"
2626
+ } : {}
2442
2627
  }, parsed.data) : {
2443
2628
  success: true,
2444
2629
  data: {}
2445
2630
  };
2446
2631
  if (!handlerResult.success) throw handlerResult.error != null ? new Error(handlerResult.error) : new HostError("Action handler failed");
2447
2632
  const handlerData = handlerResult.data ?? {};
2633
+ let adapterPreparation;
2634
+ if (handlerResult.adapterPreparation !== void 0) {
2635
+ if (!action.finalize) throw new HostError("adapterPreparation requires a finalizing action.");
2636
+ const value = handlerResult.adapterPreparation;
2637
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new HostError("adapterPreparation must be a plain JSON object.");
2638
+ adapterPreparation = finalizationJson(value);
2639
+ }
2448
2640
  const rawStagedInvocations = handlerResult.stagedInvocations ?? [];
2449
2641
  const stagedRecords = prepareStagedInvocationBatch({
2450
2642
  parent: invocation,
@@ -2498,12 +2690,12 @@ function createGovernedActionHost(options) {
2498
2690
  if (preparedFinalizationInput) {
2499
2691
  const skipped = [];
2500
2692
  for (const [index, step] of (action.adapterSteps ?? []).entries()) {
2501
- const input = step.getInput(structuredClone(preparedFinalizationInput.params), structuredClone(preparedFinalizationInput.handlerData));
2693
+ const input = step.getInput(structuredClone(preparedFinalizationInput.params), structuredClone(preparedFinalizationInput.handlerData), structuredClone(adapterPreparation));
2502
2694
  preparedInputs.set(index, input);
2503
2695
  if (!input) skipped.push(index);
2504
2696
  }
2505
2697
  preparation = normalizeFinalizationJson({
2506
- protocol: "atomic-after-adapters-v1",
2698
+ protocol: adapterPreparation !== void 0 ? "atomic-after-adapters-v2" : "atomic-after-adapters-v1",
2507
2699
  actionVersion: action.version,
2508
2700
  steps: (action.adapterSteps ?? []).map(({ adapterType, operation }) => ({
2509
2701
  adapterType,
@@ -2512,6 +2704,7 @@ function createGovernedActionHost(options) {
2512
2704
  ...preparedFinalizationInput,
2513
2705
  handlerEvents: events,
2514
2706
  result: preparedFinalizationInput.handlerData,
2707
+ ...adapterPreparation !== void 0 ? { adapterPreparation } : {},
2515
2708
  ...transitionEvent ? { stateTransition: transitionEvent } : {},
2516
2709
  skipped,
2517
2710
  ...handlerStagedInvocations.length > 0 ? { stagedInvocations: handlerStagedInvocations } : {}
@@ -2661,7 +2854,7 @@ function createGovernedActionHost(options) {
2661
2854
  }, error));
2662
2855
  }
2663
2856
  if (previousAdapterInvocation?.status === "ambiguous") return await settleUnknownOutcome(stepIndex, recordedAdapterReconciliation(previousAdapterInvocation), previousAdapterInvocation.reconciliation?.adapterEventSubject);
2664
- 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);
2857
+ const input = action.finalize ? preparation.skipped.includes(stepIndex) ? void 0 : replayCheckpoint ? step.getInput(structuredClone(replayCheckpoint.params), structuredClone(replayCheckpoint.handlerData), structuredClone(replayCheckpoint.adapterPreparation)) : preparedInputs.get(stepIndex) : step.getInput(parsed.data, data);
2665
2858
  if (!input) {
2666
2859
  if (action.finalize) finalizationSteps.push({
2667
2860
  stepIndex,
@@ -3253,14 +3446,14 @@ function createGovernedActionHost(options) {
3253
3446
  ...pendingHandoff,
3254
3447
  parkedAt: now()
3255
3448
  } : void 0;
3256
- const finalPatch = parked ? {
3449
+ const finalPatch = finalizationSettlementPatch(invocation, parked ? {
3257
3450
  status: "running",
3258
3451
  result,
3259
3452
  pendingCompletion: parked
3260
3453
  } : {
3261
3454
  status: "completed",
3262
3455
  result
3263
- };
3456
+ });
3264
3457
  let verdictWritten = true;
3265
3458
  let finalizationCallbackFailure;
3266
3459
  try {
@@ -3421,6 +3614,7 @@ function createGovernedActionHost(options) {
3421
3614
  }
3422
3615
  }
3423
3616
  async function resumeApprovedInvocation(actionInvocationId, tenantId, spaceId, decision) {
3617
+ decision = snapshotApprovalDecision(decision);
3424
3618
  if (!decision.approverId.trim()) throw new Error("approverId is required");
3425
3619
  const invocation = await options.store.getActionInvocation(actionInvocationId, tenantId, spaceId);
3426
3620
  if (!invocation) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
@@ -3430,7 +3624,13 @@ function createGovernedActionHost(options) {
3430
3624
  error: `Invocation is not waiting for approval (status: ${invocation.status})`
3431
3625
  };
3432
3626
  const action = actionResolver(invocation.actionId);
3433
- if (!action) return fail(invocation, "failed", `Unknown action: ${invocation.actionId}`);
3627
+ if (!action) {
3628
+ const error = `Unknown action: ${invocation.actionId}`;
3629
+ return invocation.approvalExpiresAt === void 0 ? fail(invocation, "failed", error) : {
3630
+ ...actionResult(invocation),
3631
+ error
3632
+ };
3633
+ }
3434
3634
  const approvalAuthorizationInput = toAuthorizationInput(action, {
3435
3635
  tenantId,
3436
3636
  spaceId,
@@ -3441,7 +3641,7 @@ function createGovernedActionHost(options) {
3441
3641
  const authorized = options.authorization.authorizeApproval ? await options.authorization.authorizeApproval({
3442
3642
  ...approvalAuthorizationInput,
3443
3643
  actionInvocationId,
3444
- decision,
3644
+ decision: structuredClone(decision),
3445
3645
  invocation: structuredClone(invocation)
3446
3646
  }) : await options.authorization.authorize(approvalAuthorizationInput);
3447
3647
  if (!entitled || !authorized) return {
@@ -3466,14 +3666,15 @@ function createGovernedActionHost(options) {
3466
3666
  },
3467
3667
  leaseOwner,
3468
3668
  leaseDurationMs: options.approvalResumeLeaseDurationMs ?? 3e5,
3469
- now: transitionStartedAt
3669
+ now: transitionStartedAt,
3670
+ ...options.approvalLifecycleEvents ? { lifecycleEvent: approvalLifecycleEvent } : {}
3470
3671
  });
3471
3672
  const transitioned = transition.invocation ?? await options.store.getActionInvocation(actionInvocationId, tenantId, spaceId);
3472
3673
  if (!transition.applied || !transitioned) {
3473
3674
  if (!transitioned) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
3474
3675
  return withConsistency(actionResult(transitioned), transitioned.actionId);
3475
3676
  }
3476
- if (!decision.approved) {
3677
+ if (transitioned.approvalExpiry || !decision.approved) {
3477
3678
  emitInvocationStatusTelemetry(transitioned, "failed", options.telemetry, now(), "waiting_for_approval");
3478
3679
  return withConsistency(actionResult(transitioned), transitioned.actionId);
3479
3680
  }
@@ -3525,9 +3726,73 @@ function createGovernedActionHost(options) {
3525
3726
  consistency
3526
3727
  } : result;
3527
3728
  }
3528
- async function appendEvent(invocation, event, deduplicationKey, defaultEventSchemaVersion = 1, transaction) {
3529
- const timestamp = now();
3530
- const envelope = {
3729
+ function eventOutboxMetadata(invocation, envelope) {
3730
+ const traceContext = options.outbox?.traceContext?.(envelope);
3731
+ const payloadClassification = options.outbox?.classifyPayload(envelope) ?? "internal";
3732
+ return {
3733
+ producerModuleVersion: options.outbox?.producerModuleVersion(invocation.actionId, invocation.actionVersion) ?? String(invocation.actionVersion),
3734
+ payloadClassification,
3735
+ includeProvenance: options.outbox?.includeProvenance?.(envelope, payloadClassification) ?? payloadClassification !== "restricted",
3736
+ includeActor: options.outbox?.includeActor?.(envelope, payloadClassification) === true,
3737
+ ...traceContext ? { traceContext } : {}
3738
+ };
3739
+ }
3740
+ const approvalLifecycleEvent = (invocation, kind) => {
3741
+ let eventType;
3742
+ let payload;
3743
+ if (kind === "requested") {
3744
+ eventType = APPROVAL_REQUESTED_EVENT;
3745
+ payload = {
3746
+ route: invocation.hitlRoute,
3747
+ riskTier: invocation.hitlRiskTier,
3748
+ reason: invocation.hitlReason ?? "",
3749
+ ...invocation.approvalExpiresAt ? { expiresAt: invocation.approvalExpiresAt.toISOString() } : {}
3750
+ };
3751
+ } else if (kind === "decided") {
3752
+ eventType = APPROVAL_DECIDED_EVENT;
3753
+ const decision = invocation.approvalDecision;
3754
+ payload = {
3755
+ approved: decision.approved,
3756
+ approverId: decision.approverId,
3757
+ approverType: decision.approverType,
3758
+ ...decision.reason !== void 0 ? { reason: decision.reason } : {},
3759
+ decidedAt: decision.decidedAt.toISOString(),
3760
+ evidencePresent: decision.evidence !== void 0
3761
+ };
3762
+ } else {
3763
+ eventType = APPROVAL_EXPIRED_EVENT;
3764
+ payload = {
3765
+ expiresAt: invocation.approvalExpiry.expiresAt.toISOString(),
3766
+ expiredAt: invocation.approvalExpiry.expiredAt.toISOString()
3767
+ };
3768
+ }
3769
+ const event = buildEventEnvelope(invocation, {
3770
+ eventType,
3771
+ subjectType: "ActionInvocation",
3772
+ subjectId: invocation.id,
3773
+ payload
3774
+ }, `approval:${kind}`, 0, kind === "expired" ? invocation.approvalExpiry.expiredAt : invocation.updatedAt);
3775
+ return {
3776
+ event,
3777
+ metadata: eventOutboxMetadata(invocation, event)
3778
+ };
3779
+ };
3780
+ async function expireDueApprovals(input = {}) {
3781
+ const store = asApprovalStore(options.store);
3782
+ if (!store?.expireDueApprovals) {
3783
+ if (options.approvalDefaultTtlMs !== void 0 || options.approvalLifecycleEvents) throw new HostError("Approval expiry requires a store with approval expiry support.");
3784
+ return { expired: [] };
3785
+ }
3786
+ const expired = await store.expireDueApprovals({
3787
+ ...input,
3788
+ now: input.now ?? now(),
3789
+ ...options.approvalLifecycleEvents ? { lifecycleEvent: approvalLifecycleEvent } : {}
3790
+ });
3791
+ for (const invocation of expired) emitInvocationStatusTelemetry(invocation, "failed", options.telemetry, invocation.approvalExpiry.expiredAt, "waiting_for_approval");
3792
+ return { expired: expired.map((invocation) => invocation.id) };
3793
+ }
3794
+ function buildEventEnvelope(invocation, event, deduplicationKey, sequence, timestamp, defaultEventSchemaVersion = 1) {
3795
+ return {
3531
3796
  id: lifecycleId("evt", invocation.id, deduplicationKey),
3532
3797
  tenantId: invocation.tenantId,
3533
3798
  spaceId: invocation.spaceId,
@@ -3539,7 +3804,7 @@ function createGovernedActionHost(options) {
3539
3804
  actorType: invocation.actorType,
3540
3805
  actionInvocationId: invocation.id,
3541
3806
  payload: event.payload,
3542
- sequence: await (transaction ?? options.store).nextEventSequence(invocation.tenantId, invocation.spaceId),
3807
+ sequence,
3543
3808
  occurredAt: timestamp,
3544
3809
  recordedAt: timestamp,
3545
3810
  correlationId: invocation.correlationId,
@@ -3550,6 +3815,10 @@ function createGovernedActionHost(options) {
3550
3815
  } } : {},
3551
3816
  ...declaredConsistency(invocation.actionId) === "provisional-until-reconciled" ? { consistency: "provisional-until-reconciled" } : {}
3552
3817
  };
3818
+ }
3819
+ async function appendEvent(invocation, event, deduplicationKey, defaultEventSchemaVersion = 1, transaction) {
3820
+ const timestamp = now();
3821
+ const envelope = buildEventEnvelope(invocation, event, deduplicationKey, await (transaction ?? options.store).nextEventSequence(invocation.tenantId, invocation.spaceId), timestamp, defaultEventSchemaVersion);
3553
3822
  if (options.outbox) {
3554
3823
  const hostLifecycleEvent = (/* @__PURE__ */ new Set([
3555
3824
  "AdapterInvocationStarted",
@@ -3565,15 +3834,7 @@ function createGovernedActionHost(options) {
3565
3834
  await (transaction ?? options.store).appendEvent(envelope);
3566
3835
  return;
3567
3836
  }
3568
- const traceContext = options.outbox.traceContext?.(envelope);
3569
- const payloadClassification = options.outbox.classifyPayload(envelope);
3570
- const metadata = {
3571
- producerModuleVersion: options.outbox.producerModuleVersion(invocation.actionId, invocation.actionVersion),
3572
- payloadClassification,
3573
- includeProvenance: options.outbox.includeProvenance?.(envelope, payloadClassification) ?? payloadClassification !== "restricted",
3574
- includeActor: options.outbox.includeActor?.(envelope, payloadClassification) === true,
3575
- ...traceContext ? { traceContext } : {}
3576
- };
3837
+ const metadata = eventOutboxMetadata(invocation, envelope);
3577
3838
  if (transaction) {
3578
3839
  if (!transaction.appendEventWithOutbox) throw new HostError("Outbox egress requires an atomic transaction with appendEventWithOutbox.");
3579
3840
  await transaction.appendEventWithOutbox(envelope, metadata);
@@ -3636,6 +3897,7 @@ function createGovernedActionHost(options) {
3636
3897
  return !evidenceOnly;
3637
3898
  }
3638
3899
  async function persistInvocation(invocation, patch, onPreservedReconciliation, lease) {
3900
+ patch = finalizationSettlementPatch(invocation, patch);
3639
3901
  const previousStatus = invocation.status;
3640
3902
  const identity = lease ?? (invocation.leaseOwner && invocation.leaseToken !== void 0 ? {
3641
3903
  leaseOwner: invocation.leaseOwner,
@@ -3951,6 +4213,7 @@ function createGovernedActionHost(options) {
3951
4213
  ...stagedStore ? { dispatchStagedInvocations } : {},
3952
4214
  executeInvocation,
3953
4215
  resumeApprovedInvocation,
4216
+ expireDueApprovals,
3954
4217
  recordExecutionAttestation,
3955
4218
  recordExternalReconciliation,
3956
4219
  completeExternalInvocation: externalCompletion.completeExternalInvocation,
@@ -4278,6 +4541,8 @@ function emitRelayTelemetry(telemetry, name, occurredAt, workerId, attributes) {
4278
4541
  var MemoryPlatformHostStore = class {
4279
4542
  db;
4280
4543
  transactionalOutbox;
4544
+ transactionalApprovalLifecycle;
4545
+ approvalTransactionProvider;
4281
4546
  transactionalStagedInvocations;
4282
4547
  adapterCommit;
4283
4548
  transactionTail = Promise.resolve();
@@ -4300,6 +4565,11 @@ var MemoryPlatformHostStore = class {
4300
4565
  constructor(db, transactionProvider) {
4301
4566
  this.db = db;
4302
4567
  this.transactionalOutbox = transactionProvider !== void 0;
4568
+ this.transactionalApprovalLifecycle = transactionProvider !== void 0;
4569
+ this.approvalTransactionProvider = transactionProvider ?? {
4570
+ snapshot: (db) => db,
4571
+ restore: () => void 0
4572
+ };
4303
4573
  this.transactionalStagedInvocations = transactionProvider !== void 0;
4304
4574
  if (transactionProvider) this.transactionWithEvents = (run) => this.runTransactionWithEvents(run, transactionProvider);
4305
4575
  }
@@ -4546,6 +4816,15 @@ var MemoryPlatformHostStore = class {
4546
4816
  const record = this.invocations.find((row) => row.id === id && row.tenantId === tenantId && row.spaceId === spaceId);
4547
4817
  if (!record) throw new Error(`ActionInvocation not found: ${id}`);
4548
4818
  Object.assign(record, patch, { updatedAt: /* @__PURE__ */ new Date() });
4819
+ for (const key of [
4820
+ "approvalExpiresAt",
4821
+ "approvalExpiry",
4822
+ "approvalDecision"
4823
+ ]) {
4824
+ if (!Object.hasOwn(patch, key)) continue;
4825
+ if (patch[key] === void 0) delete record[key];
4826
+ else Object.assign(record, { [key]: structuredClone(patch[key]) });
4827
+ }
4549
4828
  if (Object.hasOwn(patch, "finalizationCheckpoint")) {
4550
4829
  if (patch.finalizationCheckpoint) record.finalizationCheckpoint = structuredClone(patch.finalizationCheckpoint);
4551
4830
  else delete record.finalizationCheckpoint;
@@ -4570,6 +4849,8 @@ var MemoryPlatformHostStore = class {
4570
4849
  async recordHitlDecision(actionInvocationId, tenantId, spaceId, decision) {
4571
4850
  const record = await this.getActionInvocation(actionInvocationId, tenantId, spaceId);
4572
4851
  if (!record) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
4852
+ if (record.hitlRoute || !["pending", "running"].includes(record.status)) return record;
4853
+ if (decision.approvalExpiresAt) record.approvalExpiresAt = new Date(decision.approvalExpiresAt);
4573
4854
  record.hitlRoute = decision.route;
4574
4855
  record.hitlRiskTier = decision.riskTier;
4575
4856
  record.hitlReason = decision.reason;
@@ -4577,34 +4858,34 @@ var MemoryPlatformHostStore = class {
4577
4858
  record.updatedAt = decision.evaluatedAt;
4578
4859
  return record;
4579
4860
  }
4861
+ runApprovalTransaction(run) {
4862
+ return this.transactionWithEvents ? this.transactionWithEvents(run) : this.runTransactionWithEvents(run, this.approvalTransactionProvider);
4863
+ }
4864
+ async parkApproval(input) {
4865
+ if (input.lifecycleEvent && !this.transactionalApprovalLifecycle) throw new Error("Approval events require transactional approval support.");
4866
+ return this.runApprovalTransaction((tx) => applyApprovalTransition(tx, input, "requested"));
4867
+ }
4580
4868
  async beginApprovalDecision(input) {
4581
- const record = await this.getActionInvocation(input.actionInvocationId, input.tenantId, input.spaceId);
4582
- if (!record || record.status !== "waiting_for_approval") return {
4583
- applied: false,
4584
- ...record ? { invocation: record } : {}
4585
- };
4586
- record.approvalDecision = input.decision;
4587
- record.updatedAt = input.now;
4588
- if (!input.decision.approved) {
4589
- record.status = "failed";
4590
- record.error = `Approval rejected: ${input.decision.reason ?? "no reason provided"}`;
4591
- delete record.leaseOwner;
4592
- delete record.leaseExpiresAt;
4593
- return {
4594
- applied: true,
4595
- invocation: record
4596
- };
4597
- }
4598
- record.status = "running";
4599
- record.error = void 0;
4600
- record.leaseOwner = input.leaseOwner;
4601
- record.leaseExpiresAt = new Date(input.now.getTime() + input.leaseDurationMs);
4602
- record.leaseToken = (record.leaseToken ?? 0) + 1;
4603
- record.attemptCount = Math.max(record.attemptCount, 1);
4604
- return {
4605
- applied: true,
4606
- invocation: record
4607
- };
4869
+ if (input.lifecycleEvent && !this.transactionalApprovalLifecycle) throw new Error("Approval events require transactional approval support.");
4870
+ return this.runApprovalTransaction((tx) => applyApprovalTransition(tx, input, "decided"));
4871
+ }
4872
+ async expireDueApprovals(input) {
4873
+ const limit = approvalSweepLimit(input);
4874
+ if (input.lifecycleEvent && !this.transactionalApprovalLifecycle) throw new Error("Approval events require transactional approval support.");
4875
+ const candidates = this.invocations.filter((row) => row.status === "waiting_for_approval" && row.approvalExpiresAt && row.approvalExpiresAt <= input.now && (input.tenantId === void 0 || row.tenantId === input.tenantId) && (input.spaceId === void 0 || row.spaceId === input.spaceId)).sort((a, b) => a.approvalExpiresAt.getTime() - b.approvalExpiresAt.getTime() || a.id.localeCompare(b.id)).slice(0, limit);
4876
+ return this.runApprovalTransaction(async (tx) => {
4877
+ const expired = [];
4878
+ for (const row of candidates) {
4879
+ const transition = await applyApprovalTransition(tx, {
4880
+ ...input,
4881
+ actionInvocationId: row.id,
4882
+ tenantId: row.tenantId,
4883
+ spaceId: row.spaceId
4884
+ }, "expired");
4885
+ if (transition.applied) expired.push(transition.invocation);
4886
+ }
4887
+ return expired;
4888
+ });
4608
4889
  }
4609
4890
  async appendPolicyEvaluation(record) {
4610
4891
  if (this.policyEvaluations.some((candidate) => candidate.id === record.id)) return;
@@ -5054,16 +5335,19 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
5054
5335
  sql;
5055
5336
  transactionWithEvents;
5056
5337
  transactionalOutbox;
5338
+ transactionalApprovalLifecycle;
5057
5339
  transactionalStagedInvocations;
5058
5340
  constructor(db, sql, transactionProvider) {
5059
5341
  this.db = db;
5060
5342
  this.sql = sql;
5061
5343
  this.transactionalOutbox = transactionProvider !== void 0;
5344
+ this.transactionalApprovalLifecycle = transactionProvider !== void 0;
5062
5345
  this.transactionalStagedInvocations = transactionProvider !== void 0;
5063
5346
  if (transactionProvider) this.transactionWithEvents = async (run) => transactionProvider.run(async ({ db, sql }) => {
5064
5347
  const scoped = new PostgresPlatformHostStore(db, sql);
5065
5348
  return run({
5066
5349
  db,
5350
+ beginApprovalDecision: (input) => scoped.beginApprovalDecision(input),
5067
5351
  getActionInvocationForUpdate: async (id, tenantId, spaceId) => {
5068
5352
  const result = await sql.query(`SELECT * FROM fabric_platform.action_invocations
5069
5353
  WHERE id=$1 AND tenant_id=$2 AND space_id=$3
@@ -5322,6 +5606,18 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
5322
5606
  CREATE INDEX IF NOT EXISTS staged_invocations_parent_idx
5323
5607
  ON fabric_platform.staged_invocations (tenant_id, space_id, parent_invocation_id, created_at);
5324
5608
  `
5609
+ },
5610
+ {
5611
+ version: 12,
5612
+ name: "approval_lifecycle_expiry",
5613
+ sql: `
5614
+ ALTER TABLE fabric_platform.action_invocations
5615
+ ADD COLUMN IF NOT EXISTS approval_expires_at timestamptz,
5616
+ ADD COLUMN IF NOT EXISTS approval_expiry jsonb;
5617
+ CREATE INDEX IF NOT EXISTS action_invocations_approval_expiry_idx
5618
+ ON fabric_platform.action_invocations (approval_expires_at, id)
5619
+ WHERE status='waiting_for_approval' AND approval_expires_at IS NOT NULL;
5620
+ `
5325
5621
  }
5326
5622
  ]);
5327
5623
  }
@@ -5532,7 +5828,9 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
5532
5828
  return result.rows[0] ? toActionRecord(result.rows[0]) : void 0;
5533
5829
  }
5534
5830
  async updateActionInvocation(id, tenantId, spaceId, patch) {
5831
+ const approvalFields = approvalPatchSql(patch, 18 + (Object.hasOwn(patch, "deferredBeforeEffectAttempt") ? 2 : 0) + (Object.hasOwn(patch, "finalizationCheckpoint") ? 1 : 0));
5535
5832
  await this.sql.query(`UPDATE fabric_platform.action_invocations SET
5833
+ ${approvalFields.sql}
5536
5834
  status=COALESCE($4,status), result=COALESCE($5::jsonb,result),
5537
5835
  error=CASE WHEN $6::boolean THEN $7 ELSE error END,
5538
5836
  authorization_reconciliation=CASE WHEN $8::boolean THEN $9::jsonb ELSE authorization_reconciliation END,
@@ -5543,10 +5841,10 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
5543
5841
  ${Object.hasOwn(patch, "finalizationCheckpoint") ? `finalization_checkpoint=$${18 + (Object.hasOwn(patch, "deferredBeforeEffectAttempt") ? 2 : 0)}::jsonb,` : ""}
5544
5842
  ${Object.hasOwn(patch, "deferredBeforeEffectAttempt") ? "deferred_before_effect_attempt=CASE WHEN $18::boolean THEN $19::integer ELSE deferred_before_effect_attempt END," : ""}
5545
5843
  pending_state_transition=CASE WHEN $16::boolean THEN $17::jsonb ELSE pending_state_transition END,
5546
- 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')
5547
- THEN NULL ELSE lease_owner END,
5548
- 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')
5549
- THEN NULL ELSE lease_expires_at END,
5844
+ ${Object.hasOwn(patch, "leaseOwner") ? "" : `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')
5845
+ THEN NULL ELSE lease_owner END,`}
5846
+ ${Object.hasOwn(patch, "leaseExpiresAt") ? "" : `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')
5847
+ THEN NULL ELSE lease_expires_at END,`}
5550
5848
  updated_at=now()
5551
5849
  WHERE id=$1 AND tenant_id=$2 AND space_id=$3`, [
5552
5850
  id,
@@ -5567,14 +5865,15 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
5567
5865
  Object.hasOwn(patch, "pendingStateTransition"),
5568
5866
  patch.pendingStateTransition ? JSON.stringify(patch.pendingStateTransition) : null,
5569
5867
  ...Object.hasOwn(patch, "deferredBeforeEffectAttempt") ? [true, patch.deferredBeforeEffectAttempt ?? null] : [],
5570
- ...Object.hasOwn(patch, "finalizationCheckpoint") ? [patch.finalizationCheckpoint ? JSON.stringify(patch.finalizationCheckpoint) : null] : []
5868
+ ...Object.hasOwn(patch, "finalizationCheckpoint") ? [patch.finalizationCheckpoint ? JSON.stringify(patch.finalizationCheckpoint) : null] : [],
5869
+ ...approvalFields.values
5571
5870
  ]);
5572
5871
  }
5573
5872
  async recordHitlDecision(actionInvocationId, tenantId, spaceId, decision) {
5574
5873
  const row = (await this.sql.query(`UPDATE fabric_platform.action_invocations SET
5575
5874
  hitl_route=$4, hitl_risk_tier=$5, hitl_reason=$6,
5576
- hitl_policy_version=$7, updated_at=$8
5577
- WHERE id=$1 AND tenant_id=$2 AND space_id=$3
5875
+ hitl_policy_version=$7, updated_at=$8, approval_expires_at=$9
5876
+ WHERE id=$1 AND tenant_id=$2 AND space_id=$3 AND hitl_route IS NULL AND status IN ('pending','running')
5578
5877
  RETURNING *`, [
5579
5878
  actionInvocationId,
5580
5879
  tenantId,
@@ -5583,20 +5882,93 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
5583
5882
  decision.riskTier,
5584
5883
  decision.reason,
5585
5884
  decision.policyVersion ?? null,
5586
- decision.evaluatedAt
5885
+ decision.evaluatedAt,
5886
+ decision.approvalExpiresAt ?? null
5587
5887
  ])).rows[0];
5588
- if (!row) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
5589
- return toActionRecord(row);
5888
+ if (row) return toActionRecord(row);
5889
+ const existing = await this.getActionInvocation(actionInvocationId, tenantId, spaceId);
5890
+ if (!existing) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
5891
+ return existing;
5892
+ }
5893
+ async parkApproval(input) {
5894
+ if (this.transactionWithEvents) return this.transactionWithEvents((tx) => applyApprovalTransition(tx, input, "requested"));
5895
+ if (input.lifecycleEvent) throw new Error("Approval events require transactional approval support.");
5896
+ const result = await this.sql.query(`UPDATE fabric_platform.action_invocations SET status='waiting_for_approval',
5897
+ approval_expires_at=COALESCE($4::timestamptz,approval_expires_at), lease_owner=NULL, lease_expires_at=NULL, updated_at=$5
5898
+ WHERE id=$1 AND tenant_id=$2 AND space_id=$3 AND status IN ('pending','running') AND approval_decision IS NULL
5899
+ AND (lease_owner IS NULL OR (lease_owner=$6 AND lease_token=$7)) RETURNING *`, [
5900
+ input.actionInvocationId,
5901
+ input.tenantId,
5902
+ input.spaceId,
5903
+ input.approvalExpiresAt ?? null,
5904
+ input.now,
5905
+ input.leaseOwner ?? null,
5906
+ input.leaseToken ?? 0
5907
+ ]);
5908
+ return result.rows[0] ? {
5909
+ applied: true,
5910
+ invocation: toActionRecord(result.rows[0])
5911
+ } : { applied: false };
5912
+ }
5913
+ async expireDueApprovals(input) {
5914
+ const limit = approvalSweepLimit(input);
5915
+ if (input.lifecycleEvent && !this.transactionWithEvents) throw new Error("Approval events require transactional approval support.");
5916
+ if (!this.transactionWithEvents) return (await this.sql.query(`WITH due AS (
5917
+ SELECT id FROM fabric_platform.action_invocations WHERE status='waiting_for_approval' AND approval_expires_at <= $1
5918
+ AND ($2::text IS NULL OR tenant_id=$2) AND ($3::text IS NULL OR space_id=$3)
5919
+ ORDER BY approval_expires_at,id LIMIT $4 FOR UPDATE SKIP LOCKED
5920
+ ) UPDATE fabric_platform.action_invocations AS invocation SET status='failed', approval_decision=NULL,
5921
+ approval_expiry=jsonb_build_object('expiresAt',approval_expires_at,'expiredAt',$1::timestamptz),
5922
+ error='Approval expired before a decision was applied.', lease_owner=NULL, lease_expires_at=NULL,
5923
+ finalization_checkpoint=CASE WHEN finalization_checkpoint ? 'adapterPreparation' OR finalization_checkpoint->>'protocol'='atomic-after-adapters-v2'
5924
+ THEN (finalization_checkpoint - 'adapterPreparation') || '{"protocol":"atomic-after-adapters-v1"}'::jsonb ELSE finalization_checkpoint END,
5925
+ updated_at=$1 FROM due WHERE invocation.id=due.id RETURNING invocation.*`, [
5926
+ input.now,
5927
+ input.tenantId ?? null,
5928
+ input.spaceId ?? null,
5929
+ limit
5930
+ ])).rows.map(toActionRecord);
5931
+ const candidates = await this.sql.query(`SELECT * FROM fabric_platform.action_invocations WHERE status='waiting_for_approval' AND approval_expires_at <= $1
5932
+ AND ($2::text IS NULL OR tenant_id=$2) AND ($3::text IS NULL OR space_id=$3)
5933
+ ORDER BY approval_expires_at,id LIMIT $4`, [
5934
+ input.now,
5935
+ input.tenantId ?? null,
5936
+ input.spaceId ?? null,
5937
+ limit
5938
+ ]);
5939
+ return this.transactionWithEvents(async (tx) => {
5940
+ if (!tx.getActionInvocationForUpdate) throw new Error("Approval expiry requires locking invocation reads.");
5941
+ for (const row of candidates.rows) await tx.getActionInvocationForUpdate(String(row.id), String(row.tenant_id), String(row.space_id));
5942
+ const expired = [];
5943
+ for (const row of candidates.rows) {
5944
+ const record = toActionRecord(row);
5945
+ const transition = await applyApprovalTransition(tx, {
5946
+ ...input,
5947
+ actionInvocationId: record.id,
5948
+ tenantId: record.tenantId,
5949
+ spaceId: record.spaceId
5950
+ }, "expired");
5951
+ if (transition.applied) expired.push(transition.invocation);
5952
+ }
5953
+ return expired;
5954
+ });
5590
5955
  }
5591
5956
  async beginApprovalDecision(input) {
5957
+ if (this.transactionWithEvents) return this.transactionWithEvents((tx) => applyApprovalTransition(tx, input, "decided"));
5958
+ if (input.lifecycleEvent) throw new Error("Approval events require transactional approval support.");
5592
5959
  const result = await this.sql.query(`UPDATE fabric_platform.action_invocations SET
5593
- approval_decision=$4::jsonb,
5594
- status=CASE WHEN $5::boolean THEN 'running' ELSE 'failed' END,
5595
- error=CASE WHEN $5::boolean THEN NULL ELSE $6 END,
5596
- lease_owner=CASE WHEN $5::boolean THEN $7 ELSE NULL END,
5597
- lease_expires_at=CASE WHEN $5::boolean THEN $8::timestamptz ELSE NULL END,
5598
- lease_token=CASE WHEN $5::boolean THEN lease_token+1 ELSE lease_token END,
5599
- attempt_count=CASE WHEN $5::boolean THEN GREATEST(attempt_count,1) ELSE attempt_count END,
5960
+ approval_expiry=CASE WHEN approval_expires_at <= $9 THEN jsonb_build_object('expiresAt',approval_expires_at,'expiredAt',$9::timestamptz) ELSE approval_expiry END,
5961
+ approval_decision=CASE WHEN approval_expires_at <= $9 THEN NULL ELSE $4::jsonb END,
5962
+ status=CASE WHEN $5::boolean AND (approval_expires_at IS NULL OR approval_expires_at > $9) THEN 'running' ELSE 'failed' END,
5963
+ finalization_checkpoint=CASE WHEN $5::boolean AND (approval_expires_at IS NULL OR approval_expires_at > $9) THEN finalization_checkpoint
5964
+ WHEN finalization_checkpoint ? 'adapterPreparation' OR finalization_checkpoint->>'protocol'='atomic-after-adapters-v2'
5965
+ THEN (finalization_checkpoint - 'adapterPreparation') || '{"protocol":"atomic-after-adapters-v1"}'::jsonb
5966
+ ELSE finalization_checkpoint END,
5967
+ error=CASE WHEN $5::boolean AND (approval_expires_at IS NULL OR approval_expires_at > $9) THEN NULL WHEN approval_expires_at <= $9 THEN 'Approval expired before a decision was applied.' ELSE $6 END,
5968
+ lease_owner=CASE WHEN $5::boolean AND (approval_expires_at IS NULL OR approval_expires_at > $9) THEN $7 ELSE NULL END,
5969
+ lease_expires_at=CASE WHEN $5::boolean AND (approval_expires_at IS NULL OR approval_expires_at > $9) THEN $8::timestamptz ELSE NULL END,
5970
+ lease_token=CASE WHEN $5::boolean AND (approval_expires_at IS NULL OR approval_expires_at > $9) THEN lease_token+1 ELSE lease_token END,
5971
+ attempt_count=CASE WHEN $5::boolean AND (approval_expires_at IS NULL OR approval_expires_at > $9) THEN GREATEST(attempt_count,1) ELSE attempt_count END,
5600
5972
  updated_at=$9
5601
5973
  WHERE id=$1 AND tenant_id=$2 AND space_id=$3 AND status='waiting_for_approval'
5602
5974
  RETURNING *`, [
@@ -6322,7 +6694,9 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
6322
6694
  }
6323
6695
  async updateLeasedActionInvocation(input) {
6324
6696
  const patch = input.patch;
6697
+ const approvalFields = approvalPatchSql(patch, 20 + (Object.hasOwn(patch, "deferredBeforeEffectAttempt") ? 2 : 0) + (Object.hasOwn(patch, "finalizationCheckpoint") ? 1 : 0));
6325
6698
  return (await this.sql.query(`UPDATE fabric_platform.action_invocations SET
6699
+ ${approvalFields.sql}
6326
6700
  status=COALESCE($6,status), result=COALESCE($7::jsonb,result),
6327
6701
  error=CASE WHEN $8::boolean THEN $9 ELSE error END,
6328
6702
  authorization_reconciliation=CASE WHEN $10::boolean THEN $11::jsonb ELSE authorization_reconciliation END,
@@ -6333,10 +6707,10 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
6333
6707
  ${Object.hasOwn(patch, "finalizationCheckpoint") ? `finalization_checkpoint=$${20 + (Object.hasOwn(patch, "deferredBeforeEffectAttempt") ? 2 : 0)}::jsonb,` : ""}
6334
6708
  ${Object.hasOwn(patch, "deferredBeforeEffectAttempt") ? "deferred_before_effect_attempt=CASE WHEN $20::boolean THEN $21::integer ELSE deferred_before_effect_attempt END," : ""}
6335
6709
  pending_state_transition=CASE WHEN $18::boolean THEN $19::jsonb ELSE pending_state_transition END,
6336
- 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')
6337
- THEN NULL ELSE lease_owner END,
6338
- 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')
6339
- THEN NULL ELSE lease_expires_at END,
6710
+ ${Object.hasOwn(patch, "leaseOwner") ? "" : `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')
6711
+ THEN NULL ELSE lease_owner END,`}
6712
+ ${Object.hasOwn(patch, "leaseExpiresAt") ? "" : `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')
6713
+ THEN NULL ELSE lease_expires_at END,`}
6340
6714
  updated_at=now()
6341
6715
  WHERE id=$1 AND tenant_id=$2 AND space_id=$3
6342
6716
  AND status='running' AND lease_owner=$4 AND lease_token=$5
@@ -6361,7 +6735,8 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
6361
6735
  Object.hasOwn(patch, "pendingStateTransition"),
6362
6736
  patch.pendingStateTransition ? JSON.stringify(patch.pendingStateTransition) : null,
6363
6737
  ...Object.hasOwn(patch, "deferredBeforeEffectAttempt") ? [true, patch.deferredBeforeEffectAttempt ?? null] : [],
6364
- ...Object.hasOwn(patch, "finalizationCheckpoint") ? [patch.finalizationCheckpoint ? JSON.stringify(patch.finalizationCheckpoint) : null] : []
6738
+ ...Object.hasOwn(patch, "finalizationCheckpoint") ? [patch.finalizationCheckpoint ? JSON.stringify(patch.finalizationCheckpoint) : null] : [],
6739
+ ...approvalFields.values
6365
6740
  ])).rows.length === 1;
6366
6741
  }
6367
6742
  async renewActionInvocationLease(input) {
@@ -6471,6 +6846,11 @@ function toActionRecord(row) {
6471
6846
  ...row.hitl_risk_tier ? { hitlRiskTier: String(row.hitl_risk_tier) } : {},
6472
6847
  ...row.hitl_reason ? { hitlReason: String(row.hitl_reason) } : {},
6473
6848
  ...row.hitl_policy_version ? { hitlPolicyVersion: String(row.hitl_policy_version) } : {},
6849
+ ...row.approval_expires_at ? { approvalExpiresAt: new Date(row.approval_expires_at) } : {},
6850
+ ...row.approval_expiry ? { approvalExpiry: {
6851
+ expiresAt: new Date(row.approval_expiry.expiresAt),
6852
+ expiredAt: new Date(row.approval_expiry.expiredAt)
6853
+ } } : {},
6474
6854
  ...row.approval_decision ? { approvalDecision: toApprovalDecision(row.approval_decision) } : {},
6475
6855
  ...row.mutation_footprint ? { mutationFootprint: row.mutation_footprint } : {},
6476
6856
  ...row.execution_principal ? { executionPrincipal: row.execution_principal } : {},
@@ -6536,6 +6916,7 @@ function toAttestationRecord(row) {
6536
6916
  function toApprovalDecision(value) {
6537
6917
  const decision = value;
6538
6918
  return {
6919
+ ...decision.evidence !== void 0 ? { evidence: structuredClone(decision.evidence) } : {},
6539
6920
  approved: Boolean(decision.approved),
6540
6921
  approverId: String(decision.approverId),
6541
6922
  approverType: String(decision.approverType),
@@ -6629,6 +7010,30 @@ function toOutboxRequeueRecord(row, prefix) {
6629
7010
  priorLeaseToken: Number(field("prior_lease_token"))
6630
7011
  };
6631
7012
  }
7013
+ /** Additional approval-only patch bindings; omitted fields do not touch older columns. */
7014
+ function approvalPatchSql(patch, start) {
7015
+ const columns = {
7016
+ approvalExpiresAt: "approval_expires_at",
7017
+ approvalExpiry: "approval_expiry",
7018
+ approvalDecision: "approval_decision",
7019
+ leaseOwner: "lease_owner",
7020
+ leaseExpiresAt: "lease_expires_at",
7021
+ leaseToken: "lease_token",
7022
+ attemptCount: "attempt_count"
7023
+ };
7024
+ const values = [];
7025
+ const assignments = [];
7026
+ for (const key of Object.keys(columns)) {
7027
+ if (!Object.hasOwn(patch, key)) continue;
7028
+ const json = key === "approvalExpiry" || key === "approvalDecision";
7029
+ values.push(patch[key] === void 0 ? null : json ? JSON.stringify(patch[key]) : patch[key]);
7030
+ assignments.push(`${columns[key]}=$${start + values.length - 1}${json ? "::jsonb" : ""},`);
7031
+ }
7032
+ return {
7033
+ sql: assignments.join("\n"),
7034
+ values
7035
+ };
7036
+ }
6632
7037
  //#endregion
6633
7038
  //#region src/worker.ts
6634
7039
  const DEFAULT_BATCH_SIZE = 10;
@@ -6670,6 +7075,16 @@ async function runPlatformActionWorkerCycle(options) {
6670
7075
  } catch (error) {
6671
7076
  options.onError?.(error);
6672
7077
  }
7078
+ let expiredApprovals = 0;
7079
+ if (typeof options.host.expireDueApprovals === "function") try {
7080
+ expiredApprovals = (await options.host.expireDueApprovals({
7081
+ ...options.tenantId !== void 0 ? { tenantId: options.tenantId } : {},
7082
+ ...options.spaceId !== void 0 ? { spaceId: options.spaceId } : {},
7083
+ limit: options.batchSize ?? DEFAULT_BATCH_SIZE
7084
+ })).expired.length;
7085
+ } catch (error) {
7086
+ options.onError?.(error);
7087
+ }
6673
7088
  let claimed;
6674
7089
  try {
6675
7090
  claimed = await options.store.claimActionInvocations({
@@ -6718,6 +7133,7 @@ async function runPlatformActionWorkerCycle(options) {
6718
7133
  claimed: claimed.length,
6719
7134
  completed,
6720
7135
  failed,
7136
+ ...expiredApprovals > 0 ? { expiredApprovals } : {},
6721
7137
  ...waitingForApproval > 0 ? { waitingForApproval } : {},
6722
7138
  ...deferred > 0 ? { deferred } : {},
6723
7139
  ...staged && Object.values(staged).some((value) => value > 0) ? { staged } : {}
@@ -6726,6 +7142,7 @@ async function runPlatformActionWorkerCycle(options) {
6726
7142
  claimed: result.claimed,
6727
7143
  completed: result.completed,
6728
7144
  failed: result.failed,
7145
+ ...result.expiredApprovals !== void 0 ? { expiredApprovals: result.expiredApprovals } : {},
6729
7146
  ...result.waitingForApproval !== void 0 ? { waitingForApproval: result.waitingForApproval } : {},
6730
7147
  ...result.deferred !== void 0 ? { deferred: result.deferred } : {},
6731
7148
  ...result.staged ? {
@@ -6855,7 +7272,7 @@ function createDurableSagaParentLifecycle(options) {
6855
7272
  correlationId: input.parentInvocationId,
6856
7273
  causationId: lifecycleId
6857
7274
  });
6858
- if (patch) await transaction.updateActionInvocation(input.parentInvocationId, input.tenantId, input.spaceId, patch);
7275
+ if (patch) await transaction.updateActionInvocation(input.parentInvocationId, input.tenantId, input.spaceId, finalizationSettlementPatch(parent, patch));
6859
7276
  });
6860
7277
  }
6861
7278
  return {
@@ -7934,6 +8351,6 @@ async function runEventAppendChecks(subject) {
7934
8351
  };
7935
8352
  }
7936
8353
  //#endregion
7937
- export { ActionAdmissionError, ExternalCompletionError, IdempotencyConflictError, MAX_STAGED_INVOCATIONS, MAX_STAGED_INVOCATION_BATCH_BYTES, MAX_STAGED_INVOCATION_BYTES, MAX_STAGED_INVOCATION_KEY_LENGTH, MemoryPlatformHostStore, PARAMETER_DIGEST_ALGORITHM, PLATFORM_HOST_CONTRACT_VERSION, PLATFORM_HOST_HEALTH_CONTRACT_VERSION, PLATFORM_HOST_METRIC_NAMES, PostgresPlatformHostStore, STAGED_INVOCATION_KEY_PATTERN, SubmissionRefusalError, applyPostgresMigrations, asStagedInvocationStore, canonicalJson, cloneOutboxRecord, cloneStagedInvocationRecord, createDurableSagaParentLifecycle, createGovernedActionHost, createStoreBackedActionDispatcher, digestParameters, emitPlatformHostTelemetry, eventAppendChecks, getPlatformHostHealthSnapshot, outboxActorDisclosure, prepareStagedInvocationBatch, runEventAppendChecks, runOutboxRelayCycle, runPlatformActionWorker, runPlatformActionWorkerCycle, runTenantIsolationChecks, stagedInvocationRequestIdentity, tenantIsolationChecks, toEnterpriseEventEnvelope };
8354
+ export { APPROVAL_DECIDED_EVENT, APPROVAL_EXPIRED_EVENT, APPROVAL_REQUESTED_EVENT, ActionAdmissionError, ExternalCompletionError, IdempotencyConflictError, MAX_APPROVAL_EVIDENCE_BYTES, MAX_STAGED_INVOCATIONS, MAX_STAGED_INVOCATION_BATCH_BYTES, MAX_STAGED_INVOCATION_BYTES, MAX_STAGED_INVOCATION_KEY_LENGTH, MemoryPlatformHostStore, PARAMETER_DIGEST_ALGORITHM, PLATFORM_HOST_CONTRACT_VERSION, PLATFORM_HOST_HEALTH_CONTRACT_VERSION, PLATFORM_HOST_METRIC_NAMES, PostgresPlatformHostStore, STAGED_INVOCATION_KEY_PATTERN, SubmissionRefusalError, applyPostgresMigrations, asStagedInvocationStore, canonicalJson, cloneOutboxRecord, cloneStagedInvocationRecord, createDurableSagaParentLifecycle, createGovernedActionHost, createStoreBackedActionDispatcher, digestParameters, emitPlatformHostTelemetry, eventAppendChecks, getPlatformHostHealthSnapshot, outboxActorDisclosure, prepareStagedInvocationBatch, runEventAppendChecks, runOutboxRelayCycle, runPlatformActionWorker, runPlatformActionWorkerCycle, runTenantIsolationChecks, stagedInvocationRequestIdentity, tenantIsolationChecks, toEnterpriseEventEnvelope };
7938
8355
 
7939
8356
  //# sourceMappingURL=index.js.map