@fabricorg/platform-host 7.1.0 → 7.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -348,6 +348,14 @@ interface ExternalCompletion {
348
348
  }
349
349
  interface RecordedExternalCompletion extends ExternalCompletion {
350
350
  recordedAt: Date;
351
+ /**
352
+ * Canonical digest of provider, reference, outcome, result, error, and
353
+ * evidence. A repeat with the same digest is the same news; a repeat with
354
+ * the same outcome and a different digest is changed evidence, and is a
355
+ * contradiction like a flipped outcome. Observation time is left out,
356
+ * since a redelivery legitimately carries a new one.
357
+ */
358
+ digest: string;
351
359
  }
352
360
  interface ReconcileOverdueCompletionsInput {
353
361
  tenantId?: string;
package/dist/index.d.ts CHANGED
@@ -348,6 +348,14 @@ interface ExternalCompletion {
348
348
  }
349
349
  interface RecordedExternalCompletion extends ExternalCompletion {
350
350
  recordedAt: Date;
351
+ /**
352
+ * Canonical digest of provider, reference, outcome, result, error, and
353
+ * evidence. A repeat with the same digest is the same news; a repeat with
354
+ * the same outcome and a different digest is changed evidence, and is a
355
+ * contradiction like a flipped outcome. Observation time is left out,
356
+ * since a redelivery legitimately carries a new one.
357
+ */
358
+ digest: string;
351
359
  }
352
360
  interface ReconcileOverdueCompletionsInput {
353
361
  tenantId?: string;
package/dist/index.js CHANGED
@@ -282,6 +282,221 @@ function isPromiseLike(value) {
282
282
  );
283
283
  }
284
284
 
285
+ // src/host-internals.ts
286
+ function asAtomicMutationStore(store) {
287
+ const candidate = store;
288
+ return typeof candidate.transactionWithEvents === "function" ? store : void 0;
289
+ }
290
+ function asGovernanceStore(store) {
291
+ const candidate = store;
292
+ return typeof candidate.recordMutationGovernance === "function" && typeof candidate.appendPolicyObligations === "function" && typeof candidate.appendExecutionAttestation === "function" ? store : void 0;
293
+ }
294
+ function withoutPrivateHostFields(data, eventResultFields) {
295
+ return Object.fromEntries(
296
+ Object.entries(data).filter(([key]) => !eventResultFields.includes(key))
297
+ );
298
+ }
299
+ function errorMessage(error) {
300
+ return error instanceof Error ? error.message : String(error);
301
+ }
302
+ function lifecycleId(prefix, invocationId, key) {
303
+ const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 96);
304
+ return `${prefix}_${invocationId}_${safeKey}`;
305
+ }
306
+ function emitInvocationStatusTelemetry(invocation, status, telemetry, occurredAt, previousStatus) {
307
+ if (!status) return;
308
+ const event = status === "completed" ? { name: "invocation.completed", metricName: PLATFORM_HOST_METRIC_NAMES.invocationCompleted } : status === "blocked_by_policy" ? { name: "invocation.policy_blocked", metricName: PLATFORM_HOST_METRIC_NAMES.invocationPolicyBlocked } : status === "validation_failed" ? { name: "invocation.validation_failed", metricName: PLATFORM_HOST_METRIC_NAMES.invocationValidationFailed } : status === "waiting_for_approval" ? { name: "invocation.approval_waited", metricName: PLATFORM_HOST_METRIC_NAMES.invocationApprovalWaited } : status === "reconciliation_required" ? { name: "invocation.reconciliation_required", metricName: PLATFORM_HOST_METRIC_NAMES.invocationReconciliationRequired } : status === "failed" ? { name: "invocation.failed", metricName: PLATFORM_HOST_METRIC_NAMES.invocationFailed } : void 0;
309
+ if (!event) return;
310
+ emitPlatformHostTelemetry(telemetry, {
311
+ kind: "event",
312
+ name: event.name,
313
+ metricName: event.metricName,
314
+ occurredAt,
315
+ tenantId: invocation.tenantId,
316
+ spaceId: invocation.spaceId,
317
+ attributes: {
318
+ ...invocation.actionId ? { actionId: invocation.actionId } : {},
319
+ ...invocation.actionVersion !== void 0 ? { actionVersion: invocation.actionVersion } : {},
320
+ ...previousStatus ? { fromStatus: previousStatus } : {}
321
+ }
322
+ });
323
+ }
324
+
325
+ // src/external-completion.ts
326
+ function createExternalCompletionLifecycle(deps) {
327
+ const { now, actionResolver, appendEvent, persistInvocation, actionResult: actionResult2, withConsistency, eventResultFields } = deps;
328
+ async function completeExternalInvocation(actionInvocationId, tenantId, spaceId, completion) {
329
+ if (typeof completion.externalReference !== "string" || completion.externalReference.trim() === "") {
330
+ throw new ExternalCompletionError("invalid_completion", "External completion must name the external reference it completes.");
331
+ }
332
+ if (completion.outcome !== "completed" && completion.outcome !== "failed") {
333
+ throw new ExternalCompletionError("invalid_completion", `External completion outcome must be "completed" or "failed", received "${String(completion.outcome)}".`);
334
+ }
335
+ const atomicStore = asAtomicMutationStore(deps.store);
336
+ if (atomicStore) {
337
+ return atomicStore.transactionWithEvents(async (transaction) => {
338
+ const read2 = transaction.getActionInvocationForUpdate ? await transaction.getActionInvocationForUpdate(actionInvocationId, tenantId, spaceId) : await deps.store.getActionInvocation(actionInvocationId, tenantId, spaceId);
339
+ return settleExternalCompletion(read2, actionInvocationId, completion, {
340
+ update: (patch) => transaction.updateActionInvocation(actionInvocationId, tenantId, spaceId, patch),
341
+ transaction
342
+ });
343
+ });
344
+ }
345
+ const read = await deps.store.getActionInvocation(actionInvocationId, tenantId, spaceId);
346
+ return settleExternalCompletion(read, actionInvocationId, completion, {
347
+ update: (patch) => deps.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, patch)
348
+ });
349
+ }
350
+ async function settleExternalCompletion(invocation, actionInvocationId, completion, writer) {
351
+ if (!invocation) throw new ExternalCompletionError("not_found", `ActionInvocation not found: ${actionInvocationId}`);
352
+ const recorded = invocation.externalCompletion;
353
+ const stillExecuting = !recorded && (invocation.status === "pending" || invocation.status === "running" && (invocation.leaseOwner !== void 0 || invocation.pendingCompletion?.parkedAt === void 0));
354
+ if (stillExecuting) {
355
+ throw new ExternalCompletionError("still_executing", `ActionInvocation ${actionInvocationId} is still executing; retry the completion after it parks.`);
356
+ }
357
+ if (recorded) {
358
+ if (recorded.externalReference !== completion.externalReference) {
359
+ throw new ExternalCompletionError("reference_mismatch", `ActionInvocation ${actionInvocationId} was completed by "${recorded.externalReference}", not "${completion.externalReference}".`);
360
+ }
361
+ if (completion.provider !== void 0 && recorded.provider !== void 0 && completion.provider !== recorded.provider) {
362
+ throw new ExternalCompletionError("provider_mismatch", `ActionInvocation ${actionInvocationId} was completed by "${recorded.provider}", not "${completion.provider}".`);
363
+ }
364
+ const incomingDigest = completionDigest(completion, recorded.provider ?? completion.provider);
365
+ if (recorded.digest === incomingDigest) {
366
+ return withConsistency(actionResult2(invocation), invocation.actionId);
367
+ }
368
+ const message = recorded.outcome === completion.outcome ? `External operation "${completion.externalReference}" reported ${completion.outcome} again with different evidence.` : `External operation "${completion.externalReference}" reported ${completion.outcome} after reporting ${recorded.outcome}.`;
369
+ await recordCompletionReconciliation(invocation, { ...completion, kind: `contradicted:${incomingDigest.slice(0, 16)}` }, message);
370
+ await appendEvent(invocation, {
371
+ eventType: "ExternalOperationContradicted",
372
+ subjectType: "AdapterInvocation",
373
+ subjectId: invocation.pendingCompletion?.adapterInvocationId ?? actionInvocationId,
374
+ payload: { externalReference: completion.externalReference, recorded: recorded.outcome, reported: completion.outcome }
375
+ }, `external:${completion.externalReference}:contradicted:${completion.outcome}`, 1, writer.transaction);
376
+ await writer.update({ status: "reconciliation_required", error: message });
377
+ emitInvocationStatusTelemetry(invocation, "reconciliation_required", deps.telemetry, now(), invocation.status);
378
+ return withConsistency(actionResult2({ ...invocation, status: "reconciliation_required", error: message }), invocation.actionId);
379
+ }
380
+ const pending = invocation.pendingCompletion;
381
+ if (!pending) {
382
+ const action = actionResolver(invocation.actionId);
383
+ if ((action?.execution?.completion ?? "immediate") === "immediate") {
384
+ throw new ExternalCompletionError("immediate_contract", `Action ${invocation.actionId} declares immediate completion; nothing external completes it.`);
385
+ }
386
+ throw new ExternalCompletionError("not_awaiting", `ActionInvocation ${actionInvocationId} is not awaiting an external completion.`);
387
+ }
388
+ if (pending.externalReference !== completion.externalReference) {
389
+ throw new ExternalCompletionError("reference_mismatch", `ActionInvocation ${actionInvocationId} awaits "${pending.externalReference}", not "${completion.externalReference}".`);
390
+ }
391
+ if (completion.provider !== void 0 && completion.provider !== pending.provider) {
392
+ throw new ExternalCompletionError("provider_mismatch", `ActionInvocation ${actionInvocationId} awaits completion from "${pending.provider}", not "${completion.provider}".`);
393
+ }
394
+ const recordedAt = now();
395
+ const externalCompletion = {
396
+ ...completion,
397
+ provider: completion.provider ?? pending.provider,
398
+ observedAt: completion.observedAt instanceof Date ? completion.observedAt.toISOString() : completion.observedAt,
399
+ recordedAt,
400
+ digest: completionDigest(completion, completion.provider ?? pending.provider)
401
+ };
402
+ const subject = { subjectType: "AdapterInvocation", subjectId: pending.adapterInvocationId };
403
+ const settles = invocation.status === "running" && pending.parkedAt !== void 0 || invocation.status === "reconciliation_required" && invocation.error?.includes("did not complete by") === true;
404
+ if (completion.outcome === "completed") {
405
+ const action = actionResolver(invocation.actionId);
406
+ const merged = withoutPrivateHostFields({ ...invocation.result, ...completion.result ?? {} }, eventResultFields);
407
+ if (action?.resultSchema) {
408
+ const parsedResult = action.resultSchema.safeParse(merged);
409
+ if (!parsedResult.success) {
410
+ throw new ExternalCompletionError("result_invalid", `External completion result validation failed at ${parsedResult.error.issues.map((issue) => issue.path.map(String).join(".") || "result").join(", ")}`);
411
+ }
412
+ }
413
+ await deps.store.updateAdapterInvocation(pending.adapterInvocationId, {
414
+ ...completion.result ? { output: withoutPrivateHostFields(completion.result, eventResultFields) } : {},
415
+ updatedAt: recordedAt
416
+ });
417
+ await appendEvent(invocation, {
418
+ eventType: "ExternalOperationCompleted",
419
+ ...subject,
420
+ payload: { externalReference: pending.externalReference, provider: pending.provider, ...completion.result ? { result: withoutPrivateHostFields(completion.result, eventResultFields) } : {} }
421
+ }, `external:${pending.adapterInvocationId}:completed`, 1, writer.transaction);
422
+ const patch2 = settles ? { status: "completed", result: merged, externalCompletion, pendingCompletion: void 0, error: void 0 } : { externalCompletion, pendingCompletion: void 0 };
423
+ await writer.update(patch2);
424
+ if (settles) emitInvocationStatusTelemetry(invocation, "completed", deps.telemetry, now(), invocation.status);
425
+ return withConsistency(actionResult2({ ...invocation, ...patch2, result: settles ? merged : invocation.result, ...settles ? { error: void 0 } : {} }), invocation.actionId);
426
+ }
427
+ const error = completion.error ?? `External operation "${pending.externalReference}" failed.`;
428
+ await deps.store.updateAdapterInvocation(pending.adapterInvocationId, { status: "failed", error, updatedAt: recordedAt });
429
+ await appendEvent(invocation, {
430
+ eventType: "ExternalOperationFailed",
431
+ ...subject,
432
+ payload: { externalReference: pending.externalReference, provider: pending.provider, error }
433
+ }, `external:${pending.adapterInvocationId}:failed`, 1, writer.transaction);
434
+ const patch = settles ? { status: "failed", error, externalCompletion, pendingCompletion: void 0 } : { externalCompletion, pendingCompletion: void 0 };
435
+ await writer.update(patch);
436
+ if (settles) emitInvocationStatusTelemetry(invocation, "failed", deps.telemetry, now(), invocation.status);
437
+ return withConsistency(actionResult2({ ...invocation, ...patch }), invocation.actionId);
438
+ }
439
+ function completionDigest(completion, provider) {
440
+ return createHash("sha256").update(canonicalJson({
441
+ provider: provider ?? null,
442
+ externalReference: completion.externalReference,
443
+ outcome: completion.outcome,
444
+ result: completion.result ?? null,
445
+ error: completion.error ?? null,
446
+ evidenceReferences: completion.evidenceReferences ?? null
447
+ })).digest("hex");
448
+ }
449
+ async function recordCompletionReconciliation(invocation, completion, reason) {
450
+ const governanceStore = asGovernanceStore(deps.store);
451
+ if (!governanceStore) throw new Error("External completion reconciliation requires a governance-capable store.");
452
+ const provider = completion.provider ?? invocation.pendingCompletion?.provider ?? invocation.externalCompletion?.provider ?? "external";
453
+ const seed = createHash("sha256").update(`${provider}\0${completion.externalReference}\0${completion.kind ?? "refused"}`).digest("hex").slice(0, 32);
454
+ await governanceStore.appendExternalReconciliation({
455
+ id: lifecycleId("rec", invocation.id, `ext:${seed}`),
456
+ actionInvocationId: invocation.id,
457
+ tenantId: invocation.tenantId,
458
+ spaceId: invocation.spaceId,
459
+ status: "pending",
460
+ provider,
461
+ externalOperationId: completion.externalReference,
462
+ attempt: 1,
463
+ reason,
464
+ ...completion.evidenceReferences ? { evidenceReferences: completion.evidenceReferences } : {},
465
+ observedAt: now()
466
+ });
467
+ }
468
+ async function reconcileOverdueCompletions(input = {}) {
469
+ const recoverable = deps.store;
470
+ if (!recoverable.listActionInvocations) {
471
+ throw new Error("Overdue completion reconciliation requires a store that can list invocations.");
472
+ }
473
+ if (!asGovernanceStore(deps.store)) {
474
+ throw new Error("Overdue completion reconciliation requires a governance-capable store to record its findings.");
475
+ }
476
+ const current = input.now ?? now();
477
+ const candidates = await recoverable.listActionInvocations({
478
+ statuses: ["running"],
479
+ completionDueBefore: current,
480
+ unleased: true,
481
+ ...input.tenantId ? { tenantId: input.tenantId } : {},
482
+ ...input.spaceId ? { spaceId: input.spaceId } : {},
483
+ limit: Math.max(1, Math.min(input.limit ?? 100, 1e3))
484
+ });
485
+ const overdue = [];
486
+ for (const invocation of candidates) {
487
+ const pending = invocation.pendingCompletion;
488
+ if (!pending?.dueAt || pending.dueAt.getTime() > current.getTime()) continue;
489
+ if (invocation.leaseOwner) continue;
490
+ const message = `External operation "${pending.externalReference}" from ${pending.provider} did not complete by ${pending.dueAt.toISOString()}.`;
491
+ await recordCompletionReconciliation(invocation, { externalReference: pending.externalReference, provider: pending.provider, kind: "overdue" }, message);
492
+ await persistInvocation(invocation, { status: "reconciliation_required", error: message });
493
+ overdue.push(invocation.id);
494
+ }
495
+ return { overdue };
496
+ }
497
+ return { completeExternalInvocation, reconcileOverdueCompletions, recordCompletionReconciliation };
498
+ }
499
+
285
500
  // src/host.ts
286
501
  var DEFAULT_EXTRACT_EVENTS = (data) => {
287
502
  const value = data._events;
@@ -1040,7 +1255,7 @@ function createGovernedActionHost(options) {
1040
1255
  });
1041
1256
  let error = refuse;
1042
1257
  try {
1043
- await recordCompletionReconciliation(invocation, { externalReference: reference, provider: adapter.vendor }, refuse);
1258
+ await externalCompletion.recordCompletionReconciliation(invocation, { externalReference: reference, provider: adapter.vendor }, refuse);
1044
1259
  } catch (logError) {
1045
1260
  error = `${refuse} Reconciliation log unavailable: ${errorMessage(logError)}`;
1046
1261
  await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, { error });
@@ -1121,7 +1336,7 @@ function createGovernedActionHost(options) {
1121
1336
  await persistInvocation(invocation, { status: "reconciliation_required", error: message });
1122
1337
  let error = message;
1123
1338
  try {
1124
- await recordCompletionReconciliation(invocation, { externalReference: pendingHandoff.externalReference, provider: pendingHandoff.provider, kind: "obligations" }, message);
1339
+ await externalCompletion.recordCompletionReconciliation(invocation, { externalReference: pendingHandoff.externalReference, provider: pendingHandoff.provider, kind: "obligations" }, message);
1125
1340
  } catch (logError) {
1126
1341
  error = `${message} Reconciliation log unavailable: ${errorMessage(logError)}`;
1127
1342
  await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, { error });
@@ -1314,163 +1529,6 @@ function createGovernedActionHost(options) {
1314
1529
  spaceId
1315
1530
  });
1316
1531
  }
1317
- async function completeExternalInvocation(actionInvocationId, tenantId, spaceId, completion) {
1318
- if (typeof completion.externalReference !== "string" || completion.externalReference.trim() === "") {
1319
- throw new ExternalCompletionError("invalid_completion", "External completion must name the external reference it completes.");
1320
- }
1321
- if (completion.outcome !== "completed" && completion.outcome !== "failed") {
1322
- throw new ExternalCompletionError("invalid_completion", `External completion outcome must be "completed" or "failed", received "${String(completion.outcome)}".`);
1323
- }
1324
- const atomicStore = asAtomicMutationStore(options.store);
1325
- if (atomicStore) {
1326
- return atomicStore.transactionWithEvents(async (transaction) => {
1327
- const read2 = transaction.getActionInvocationForUpdate ? await transaction.getActionInvocationForUpdate(actionInvocationId, tenantId, spaceId) : await options.store.getActionInvocation(actionInvocationId, tenantId, spaceId);
1328
- return settleExternalCompletion(read2, actionInvocationId, completion, {
1329
- update: (patch) => transaction.updateActionInvocation(actionInvocationId, tenantId, spaceId, patch),
1330
- transaction
1331
- });
1332
- });
1333
- }
1334
- const read = await options.store.getActionInvocation(actionInvocationId, tenantId, spaceId);
1335
- return settleExternalCompletion(read, actionInvocationId, completion, {
1336
- update: (patch) => options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, patch)
1337
- });
1338
- }
1339
- async function settleExternalCompletion(invocation, actionInvocationId, completion, writer) {
1340
- if (!invocation) throw new ExternalCompletionError("not_found", `ActionInvocation not found: ${actionInvocationId}`);
1341
- const recorded = invocation.externalCompletion;
1342
- const stillExecuting = !recorded && (invocation.status === "pending" || invocation.status === "running" && (invocation.leaseOwner !== void 0 || invocation.pendingCompletion?.parkedAt === void 0));
1343
- if (stillExecuting) {
1344
- throw new ExternalCompletionError("still_executing", `ActionInvocation ${actionInvocationId} is still executing; retry the completion after it parks.`);
1345
- }
1346
- if (recorded) {
1347
- if (recorded.externalReference !== completion.externalReference) {
1348
- throw new ExternalCompletionError("reference_mismatch", `ActionInvocation ${actionInvocationId} was completed by "${recorded.externalReference}", not "${completion.externalReference}".`);
1349
- }
1350
- if (completion.provider !== void 0 && recorded.provider !== void 0 && completion.provider !== recorded.provider) {
1351
- throw new ExternalCompletionError("provider_mismatch", `ActionInvocation ${actionInvocationId} was completed by "${recorded.provider}", not "${completion.provider}".`);
1352
- }
1353
- if (recorded.outcome === completion.outcome) {
1354
- return withConsistency(actionResult(invocation), invocation.actionId);
1355
- }
1356
- const message = `External operation "${completion.externalReference}" reported ${completion.outcome} after reporting ${recorded.outcome}.`;
1357
- await recordCompletionReconciliation(invocation, { ...completion, kind: `contradicted:${completion.outcome}` }, message);
1358
- await appendEvent(invocation, {
1359
- eventType: "ExternalOperationContradicted",
1360
- subjectType: "AdapterInvocation",
1361
- subjectId: invocation.pendingCompletion?.adapterInvocationId ?? actionInvocationId,
1362
- payload: { externalReference: completion.externalReference, recorded: recorded.outcome, reported: completion.outcome }
1363
- }, `external:${completion.externalReference}:contradicted:${completion.outcome}`, 1, writer.transaction);
1364
- await writer.update({ status: "reconciliation_required", error: message });
1365
- emitInvocationStatusTelemetry(invocation, "reconciliation_required", options.telemetry, now(), invocation.status);
1366
- return withConsistency(actionResult({ ...invocation, status: "reconciliation_required", error: message }), invocation.actionId);
1367
- }
1368
- const pending = invocation.pendingCompletion;
1369
- if (!pending) {
1370
- const action = actionResolver(invocation.actionId);
1371
- if ((action?.execution?.completion ?? "immediate") === "immediate") {
1372
- throw new ExternalCompletionError("immediate_contract", `Action ${invocation.actionId} declares immediate completion; nothing external completes it.`);
1373
- }
1374
- throw new ExternalCompletionError("not_awaiting", `ActionInvocation ${actionInvocationId} is not awaiting an external completion.`);
1375
- }
1376
- if (pending.externalReference !== completion.externalReference) {
1377
- throw new ExternalCompletionError("reference_mismatch", `ActionInvocation ${actionInvocationId} awaits "${pending.externalReference}", not "${completion.externalReference}".`);
1378
- }
1379
- if (completion.provider !== void 0 && completion.provider !== pending.provider) {
1380
- throw new ExternalCompletionError("provider_mismatch", `ActionInvocation ${actionInvocationId} awaits completion from "${pending.provider}", not "${completion.provider}".`);
1381
- }
1382
- const recordedAt = now();
1383
- const externalCompletion = {
1384
- ...completion,
1385
- provider: completion.provider ?? pending.provider,
1386
- observedAt: completion.observedAt instanceof Date ? completion.observedAt.toISOString() : completion.observedAt,
1387
- recordedAt
1388
- };
1389
- const subject = { subjectType: "AdapterInvocation", subjectId: pending.adapterInvocationId };
1390
- const settles = invocation.status === "running" && pending.parkedAt !== void 0 || invocation.status === "reconciliation_required" && invocation.error?.includes("did not complete by") === true;
1391
- if (completion.outcome === "completed") {
1392
- const action = actionResolver(invocation.actionId);
1393
- const merged = withoutPrivateHostFields({ ...invocation.result, ...completion.result ?? {} }, eventResultFields);
1394
- if (action?.resultSchema) {
1395
- const parsedResult = action.resultSchema.safeParse(merged);
1396
- if (!parsedResult.success) {
1397
- throw new ExternalCompletionError("result_invalid", `External completion result validation failed at ${parsedResult.error.issues.map((issue) => issue.path.map(String).join(".") || "result").join(", ")}`);
1398
- }
1399
- }
1400
- await options.store.updateAdapterInvocation(pending.adapterInvocationId, {
1401
- ...completion.result ? { output: withoutPrivateHostFields(completion.result, eventResultFields) } : {},
1402
- updatedAt: recordedAt
1403
- });
1404
- await appendEvent(invocation, {
1405
- eventType: "ExternalOperationCompleted",
1406
- ...subject,
1407
- payload: { externalReference: pending.externalReference, provider: pending.provider, ...completion.result ? { result: withoutPrivateHostFields(completion.result, eventResultFields) } : {} }
1408
- }, `external:${pending.adapterInvocationId}:completed`, 1, writer.transaction);
1409
- const patch2 = settles ? { status: "completed", result: merged, externalCompletion, pendingCompletion: void 0, error: void 0 } : { externalCompletion, pendingCompletion: void 0 };
1410
- await writer.update(patch2);
1411
- if (settles) emitInvocationStatusTelemetry(invocation, "completed", options.telemetry, now(), invocation.status);
1412
- return withConsistency(actionResult({ ...invocation, ...patch2, result: settles ? merged : invocation.result, ...settles ? { error: void 0 } : {} }), invocation.actionId);
1413
- }
1414
- const error = completion.error ?? `External operation "${pending.externalReference}" failed.`;
1415
- await options.store.updateAdapterInvocation(pending.adapterInvocationId, { status: "failed", error, updatedAt: recordedAt });
1416
- await appendEvent(invocation, {
1417
- eventType: "ExternalOperationFailed",
1418
- ...subject,
1419
- payload: { externalReference: pending.externalReference, provider: pending.provider, error }
1420
- }, `external:${pending.adapterInvocationId}:failed`, 1, writer.transaction);
1421
- const patch = settles ? { status: "failed", error, externalCompletion, pendingCompletion: void 0 } : { externalCompletion, pendingCompletion: void 0 };
1422
- await writer.update(patch);
1423
- if (settles) emitInvocationStatusTelemetry(invocation, "failed", options.telemetry, now(), invocation.status);
1424
- return withConsistency(actionResult({ ...invocation, ...patch }), invocation.actionId);
1425
- }
1426
- async function recordCompletionReconciliation(invocation, completion, reason) {
1427
- const governanceStore = asGovernanceStore(options.store);
1428
- if (!governanceStore) throw new Error("External completion reconciliation requires a governance-capable store.");
1429
- const provider = completion.provider ?? invocation.pendingCompletion?.provider ?? invocation.externalCompletion?.provider ?? "external";
1430
- const seed = createHash("sha256").update(`${provider}\0${completion.externalReference}\0${completion.kind ?? "refused"}`).digest("hex").slice(0, 32);
1431
- await governanceStore.appendExternalReconciliation({
1432
- id: lifecycleId("rec", invocation.id, `ext:${seed}`),
1433
- actionInvocationId: invocation.id,
1434
- tenantId: invocation.tenantId,
1435
- spaceId: invocation.spaceId,
1436
- status: "pending",
1437
- provider,
1438
- externalOperationId: completion.externalReference,
1439
- attempt: 1,
1440
- reason,
1441
- ...completion.evidenceReferences ? { evidenceReferences: completion.evidenceReferences } : {},
1442
- observedAt: now()
1443
- });
1444
- }
1445
- async function reconcileOverdueCompletions(input = {}) {
1446
- const recoverable = options.store;
1447
- if (!recoverable.listActionInvocations) {
1448
- throw new Error("Overdue completion reconciliation requires a store that can list invocations.");
1449
- }
1450
- if (!asGovernanceStore(options.store)) {
1451
- throw new Error("Overdue completion reconciliation requires a governance-capable store to record its findings.");
1452
- }
1453
- const current = input.now ?? now();
1454
- const candidates = await recoverable.listActionInvocations({
1455
- statuses: ["running"],
1456
- completionDueBefore: current,
1457
- unleased: true,
1458
- ...input.tenantId ? { tenantId: input.tenantId } : {},
1459
- ...input.spaceId ? { spaceId: input.spaceId } : {},
1460
- limit: Math.max(1, Math.min(input.limit ?? 100, 1e3))
1461
- });
1462
- const overdue = [];
1463
- for (const invocation of candidates) {
1464
- const pending = invocation.pendingCompletion;
1465
- if (!pending?.dueAt || pending.dueAt.getTime() > current.getTime()) continue;
1466
- if (invocation.leaseOwner) continue;
1467
- const message = `External operation "${pending.externalReference}" from ${pending.provider} did not complete by ${pending.dueAt.toISOString()}.`;
1468
- await recordCompletionReconciliation(invocation, { externalReference: pending.externalReference, provider: pending.provider, kind: "overdue" }, message);
1469
- await persistInvocation(invocation, { status: "reconciliation_required", error: message });
1470
- overdue.push(invocation.id);
1471
- }
1472
- return { overdue };
1473
- }
1474
1532
  function declaredConsistency(actionId) {
1475
1533
  return actionResolver(actionId)?.execution?.consistency;
1476
1534
  }
@@ -1567,7 +1625,26 @@ function createGovernedActionHost(options) {
1567
1625
  );
1568
1626
  emitInvocationStatusTelemetry(invocation, patch.status, options.telemetry, now(), previousStatus);
1569
1627
  }
1570
- return { submitAction, executeInvocation, resumeApprovedInvocation, recordExecutionAttestation, recordExternalReconciliation, completeExternalInvocation, reconcileOverdueCompletions };
1628
+ const externalCompletion = createExternalCompletionLifecycle({
1629
+ store: options.store,
1630
+ telemetry: options.telemetry,
1631
+ eventResultFields,
1632
+ now,
1633
+ actionResolver,
1634
+ appendEvent,
1635
+ persistInvocation,
1636
+ actionResult,
1637
+ withConsistency
1638
+ });
1639
+ return {
1640
+ submitAction,
1641
+ executeInvocation,
1642
+ resumeApprovedInvocation,
1643
+ recordExecutionAttestation,
1644
+ recordExternalReconciliation,
1645
+ completeExternalInvocation: externalCompletion.completeExternalInvocation,
1646
+ reconcileOverdueCompletions: externalCompletion.reconcileOverdueCompletions
1647
+ };
1571
1648
  }
1572
1649
  function actionResult(invocation) {
1573
1650
  return {
@@ -1586,18 +1663,10 @@ function asApprovalStore(store) {
1586
1663
  const candidate = store;
1587
1664
  return typeof candidate.recordHitlDecision === "function" && typeof candidate.beginApprovalDecision === "function" ? store : void 0;
1588
1665
  }
1589
- function asAtomicMutationStore(store) {
1590
- const candidate = store;
1591
- return typeof candidate.transactionWithEvents === "function" ? store : void 0;
1592
- }
1593
1666
  function asOutboxStore(store) {
1594
1667
  const candidate = store;
1595
1668
  return typeof candidate.appendEventWithOutbox === "function" && typeof candidate.claimOutbox === "function" ? store : void 0;
1596
1669
  }
1597
- function asGovernanceStore(store) {
1598
- const candidate = store;
1599
- return typeof candidate.recordMutationGovernance === "function" && typeof candidate.appendPolicyObligations === "function" && typeof candidate.appendExecutionAttestation === "function" ? store : void 0;
1600
- }
1601
1670
  function numberValue(value) {
1602
1671
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1603
1672
  }
@@ -1633,39 +1702,9 @@ function initialState(machine) {
1633
1702
  function isTerminal(status) {
1634
1703
  return ["completed", "failed", "blocked_by_policy", "reconciliation_required", "validation_failed"].includes(status);
1635
1704
  }
1636
- function emitInvocationStatusTelemetry(invocation, status, telemetry, occurredAt, previousStatus) {
1637
- if (!status) return;
1638
- const event = status === "completed" ? { name: "invocation.completed", metricName: PLATFORM_HOST_METRIC_NAMES.invocationCompleted } : status === "blocked_by_policy" ? { name: "invocation.policy_blocked", metricName: PLATFORM_HOST_METRIC_NAMES.invocationPolicyBlocked } : status === "validation_failed" ? { name: "invocation.validation_failed", metricName: PLATFORM_HOST_METRIC_NAMES.invocationValidationFailed } : status === "waiting_for_approval" ? { name: "invocation.approval_waited", metricName: PLATFORM_HOST_METRIC_NAMES.invocationApprovalWaited } : status === "reconciliation_required" ? { name: "invocation.reconciliation_required", metricName: PLATFORM_HOST_METRIC_NAMES.invocationReconciliationRequired } : status === "failed" ? { name: "invocation.failed", metricName: PLATFORM_HOST_METRIC_NAMES.invocationFailed } : void 0;
1639
- if (!event) return;
1640
- emitPlatformHostTelemetry(telemetry, {
1641
- kind: "event",
1642
- name: event.name,
1643
- metricName: event.metricName,
1644
- occurredAt,
1645
- tenantId: invocation.tenantId,
1646
- spaceId: invocation.spaceId,
1647
- attributes: {
1648
- ...invocation.actionId ? { actionId: invocation.actionId } : {},
1649
- ...invocation.actionVersion !== void 0 ? { actionVersion: invocation.actionVersion } : {},
1650
- ...previousStatus ? { fromStatus: previousStatus } : {}
1651
- }
1652
- });
1653
- }
1654
- function withoutPrivateHostFields(data, eventResultFields) {
1655
- return Object.fromEntries(
1656
- Object.entries(data).filter(([key]) => !eventResultFields.includes(key))
1657
- );
1658
- }
1659
- function errorMessage(error) {
1660
- return error instanceof Error ? error.message : String(error);
1661
- }
1662
1705
  function isRecord(value) {
1663
1706
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
1664
1707
  }
1665
- function lifecycleId(prefix, invocationId, key) {
1666
- const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 96);
1667
- return `${prefix}_${invocationId}_${safeKey}`;
1668
- }
1669
1708
  function validateAuthorizationBinding(input, parameterDigest, authorityMoment, currentTime = /* @__PURE__ */ new Date()) {
1670
1709
  const binding = input.authorizationBinding;
1671
1710
  if (binding.id !== input.authorizationBindingId && input.authorizationBindingId) throw new Error("Authorization binding ID does not match authorizationBindingId.");