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